TreeLine/0000755000175000017500000000000013760323621011246 5ustar dougdougTreeLine/translations/0000755000175000017500000000000013715364266014001 5ustar dougdougTreeLine/translations/qt_zh.qm0000600000175000017500000034505413453244177015465 0ustar dougdoug0~ee6_eEį į y~N^oxQ73m.(4B(4B(5B(5C0*yX*ym *y*TM*0'*0+Fw+F+L +f+f;+z.++m++z.++XYYKxYz9YQYAZg~ZkZLp[;^c\\]41\]4\\at#gclG|^acVv5vT$fS24Lu5.76CG<IA[Iysɵn1xɵn`aɵndɵnrɵnɵnɵnɪɵn˧(UY B* 'tM>EiuaW,qf o>\H,]<p5]#Q%UT(ŎC*4.-ct6205vXw^{x=|{yMcNGINW2zd2{'K.7R#Ai 9P` d-ytf.iU^y#[IgBurd  ~v Yd "l)*-S/=N1$5~P< g?2?NM %NkyUUiW~cXc]P`2`jt5lglyzgl}oivty0vtyP1J]"#O?l),s94G)e6w66^S{KmORT=|1~.$wϓ ?EE o {1=8AARO\[y%LX:n~o2u#BmxQMwDMEOEVLwwTV^Y I FLCr !e%B4&%)*/eH5"1;}ByKEc.,F4OZf\c`vbEcփ߲f&g&4 jCCdmnk8qq_tuZu({>}kaK0~"O\ _Q{y BFun$Ȭ$G+57(uʁr^K K*֊/ nt,a;y/O-QAnJ7^j=4&Ha>. /`?;IxS-MR>)YMPYMV^Ph^#i&Zssc|s+w$PZxFR/^q28^_~NA2Hc$\SJPqTVVFfR; M H  ^A es_$%C#I&~I&1)2h)6*4L+a,?"t?>u5J?KNYM֫R|kV|J]_]cdIgA k7y^ {y5t [FA):[ΞUvJG%Nn:ص'ǥ+)6Řt0{yS/G;LxALr9\LsL@"Ͼl%-0{GC-p5C^Yƨ dƨ˾cdtҝz-iէ?QZ>(z9xߺ`hs8fq.m}^=!  $v.~b@~bEhoMh0!b)ўQ+uW+3ʌ,8/J/A1Z4~ 6 ? 2AD!_GOGbDLAUOrPѧNQXSnTxUfHUVUT_/ZZZ(ZV[]k*@s]62^n_pseiSiYwkQ)oN%'y;sm{Z}u}w }wI}w }<'fr"#B>|vCtt.9.r399PiU'aDD;Yot(2tVt~{0"-n|_ n+Fu_C[ʢ3Qʢ(ƴd3duddd0 59b"э+] NSjU.dBhjwdj 2W ?M bc'V|O+Z,D/22742a6S7DM:r?;6CU]mDqIN9|J0KK U|]V7N>\artwKl|(^ ||T}wZ}$}$}$ ϗ{ZçVN^fD\BL>XSNqK<Hf+W·д·ý2Ƕ׳ /XF,TUENHgxVvu %5DTeh{ e~Oi~i 9%Pwb-#%%r'|-.zb.' 5kE=Շ= ?o/?rYCtIGPNV%XXU ?Zh``NvDbD$bGifBfdUgAmhII i$x1 זz*2x|QR6dJwlU (.v5z8Jc.[1-DrXmO^^_e n$Kba†5niFCʴ5wʴ5!ʶy}^ ^AԄx۔#D#'Nm4d5F5!(F5yYp]q+>BII?As 3 }$v| qeU ڤ}h ڥ dO EtE E  Ac} AcB!  35 u b bb b`^ b`  gUp i3% la{= lfq uu6 xq"% |oS | J t tA .$ ~ ` )M1 F>,[ 9 (r ~  B ҉ >̷  Z{ d t nqi Nr] Y+Ԍ K  팤X l~= %' ޽ /1( =! qK N }# o9 7V ) */? .>q 5^ 7u ;k& =N B BnE4 J" J"J K2# Rۮ 5 Ty ` T^ Uj4n ] `` ` bpI bP c(B cE d e e@B e{ f1Q/ f* g5U gn3 k, rD" t>nG :.R f  f = 4a .M- W# s] s> AAT, 9x 9w  m, #-t 0N+a 5 A CUM E9> I L( L' L9 Mc\ R^ Sb Ve W% ]$2@ f)Y f)== f=5 io>w m`" w xR/" yr5' >RI  t H H< *( nD $> .@  i  c p n3  %X J J> #I t. k Ӈ M  N>j( ̺Nc & -DO& .l ۷ c>IQ rM k+ k U)Y  <! y 0   z+  R  Y IHZ %MZ Np / C xH\ r 8>  .- 7F >Tq >U$ >U >\ >f >l >m >P ?t| DT ; I-X P@  RVBg RV RV S. SGP S Y Yz [ c`e hۮG j7o: p. vt 3 . Bda  T1 Ts3 T& T $ e/ 3 H S )d% Tq L ;>y .1 .` .s . .y .   a> y! o e.@ hNpb >X ҂$5  %1 u z 8 |Ґ l h Xtd n 9 t6 a >7 :bZ. Uqwb  O.< ʜ,  ]4 #$ #=f %nqb (I$ (N0\ +>/& +k 0E 64] ;ɾc Fg K9S Ptym Pt T>R W H dBg fe fe, g iFC i@ iV jӮ kGn m9 n'd u uL v o@ v& v{G w΂ w w * w}ΰ w} w} V |[+;  P V % J] ^{ }k R= P  xN Up ɰeh F}, ' X< &* D Ah + t5x t5 C a2 > z x ) y "Rw @afT);oAgT)4*'*n/E(/E=Bi+I_ZxKOOpXRuwX4 [ [ a.a./gcnyG*GvɅy$~[>Gn4y&m'@4Sf^Ǘ 0:B&rncݖ[y^{r h >JG3#lDb%"#o$UE%45%4D-v0i)߀01c2wTL<T<(]D#~HJd[IKK#hL$.Wbbc5dc5 g3iCmhpTyC${~a6$Y5 K&&_{j]`CnS[!>T&G>>bN:I E"~Ldr)1r?kyLn~kBLPt2+r`^dvU7ibp<html>RcbR0Ve>Y <b>%1</b> <br/>[RRSN:Su(^vNQwg fvOQH~0</html>xSwitching to the audio playback device %1
which just became available and has higher preference. AudioOutputl<html>Ve>Y <b>%1</b> lg ]O\0<br/>VnR0 <b>%2</b>0</html>^The audio playback device %1 does not work.
Falling back to %2. AudioOutput`bY R0Y %1 Revert back to device '%1' AudioOutput Qsh{~u Close Tab CloseButton QsN %1About %1MAC_APPLICATION_MENU%1Hide %1MAC_APPLICATION_MENUQvN Hide OthersMAC_APPLICATION_MENU POY}n &Preferences...MAC_APPLICATION_MENU Q %1Quit %1MAC_APPLICATION_MENUg RServicesMAC_APPLICATION_MENUQhf>y:Show AllMAC_APPLICATION_MENU exsX AccessibilityPhonon:: CommunicationPhonon::n8bGamesPhonon::NPMusicPhonon::w NotificationsPhonon::ƘVideoPhonon::ffTJw wge `lg [Wx@v GStreamer cN0 b@g vTƘe/c]~ψQs0~Warning: You do not seem to have the base GStreamer plugins installed. All audio and video support has been disabledPhonon::Gstreamer::BackendvfTJw wge `lg [ gstreamer0.10-plugins-good S0 NNƘry`']~ψQs0Warning: You do not seem to have the package gstreamer0.10-plugins-good installed. Some video features have been disabled.Phonon::Gstreamer::Backend>:\NN*vxVh0`[YN xVhgede>N*Q[%0`A required codec is missing. You need to install the following codec(s) to play this content: %0Phonon::Gstreamer::MediaObjectN _YVe>0 hg`v Gstreamer [^vNxn` ]~[ libgstreamer-plugins-base0wCannot start playback. Check your Gstreamer installation and make sure you have libgstreamer-plugins-base installed.Phonon::Gstreamer::MediaObjectN xZOSn0Could not decode media source.Phonon::Gstreamer::MediaObjectN [OMZOSn0Could not locate media source.Phonon::Gstreamer::MediaObject&N bS_󘑋Y0N*YkcW(Ou(0:Could not open audio device. The device is already in use.Phonon::Gstreamer::MediaObjectN bS_ZOSn0Could not open media source.Phonon::Gstreamer::MediaObjecteeHvn|{W0Invalid source type.Phonon::Gstreamer::MediaObject0Ou(N*nWW0g]N:%0 gSN:%1%WUse this slider to adjust the volume. The leftmost position is 0%, the rightmost is %1%Phonon::VolumeSlider %1% Volume: %1%Phonon::VolumeSlider%1 %2g*[NI%1, %2 not definedQ3AccelN fxnv%1lg YtAmbiguous %1 not handledQ3AccelR dDelete Q3DataTablePGFalse Q3DataTablecQeInsert Q3DataTablewTrue Q3DataTablefeUpdate Q3DataTable*eN%1 g*b~R00 hg_TeNT 0 +%1 File not found. Check path and filename. Q3FileDialog R d(&D)&Delete Q3FileDialog T&(&N)&No Q3FileDialog xn[(&O)&OK Q3FileDialog bS_(&O)&Open Q3FileDialogT}T (&R)&Rename Q3FileDialog O[X(&S)&Save Q3FileDialogg*cRv(&U) &Unsorted Q3FileDialog f/(&Y)&Yes Q3FileDialog0<qt>O`xnO``R d%1 %2 </qt>1Are you sure you wish to delete %1 "%2"? Q3FileDialogb@g eN (*) All Files (*) Q3FileDialogb@g eN (*.*)All Files (*.*) Q3FileDialog\^`' Attributes Q3FileDialogTBack Q3FileDialogSmCancel Q3FileDialogY R6byRNN*eNCopy or Move a File Q3FileDialog R^eeNY9Create New Folder Q3FileDialogegDate Q3FileDialogR d%1 Delete %1 Q3FileDialog~ƉV Detail View Q3FileDialogv_UDir Q3FileDialogv_U Directories Q3FileDialogv_U Directory: Q3FileDialogError Q3FileDialogeNFile Q3FileDialogeNT y(&N) File &name: Q3FileDialogeN|{W(&T) File &type: Q3FileDialoggb~v_UFind Directory Q3FileDialog N Sv Inaccessible Q3FileDialogRhV List View Q3FileDialoggb~V(&I) Look &in: Q3FileDialogT yName Q3FileDialog e^eNY9 New Folder Q3FileDialoge^eNY9%1 New Folder %1 Q3FileDialog e^eNY91 New Folder 1 Q3FileDialogTN N~One directory up Q3FileDialogbS_Open Q3FileDialogbS_Open  Q3FileDialog eNQ[Preview File Contents Q3FileDialog eNO`oPreview File Info Q3FileDialoge}Qe(&E)R&eload Q3FileDialogS Read-only Q3FileDialogQ Read-write Q3FileDialog S%1Read: %1 Q3FileDialogS[XN:Save As Q3FileDialog bNN*v_USelect a Directory Q3FileDialogf>y:eN(&H)Show &hidden files Q3FileDialogY'\Size Q3FileDialogcRSort Q3FileDialogc egcR(&D) Sort by &Date Q3FileDialogc T ycR(&N) Sort by &Name Q3FileDialogc Y'\cR(&S) Sort by &Size Q3FileDialogrykSpecial Q3FileDialogv_Uv|~ߔcSymlink to Directory Q3FileDialogeNv|~ߔcSymlink to File Q3FileDialogrykv|~ߔcSymlink to Special Q3FileDialog|{WType Q3FileDialogSQ Write-only Q3FileDialog QQe%1 Write: %1 Q3FileDialogv_U the directory Q3FileDialogeNthe file Q3FileDialog|~ߔc the symlink Q3FileDialogN R^v_U %1Could not create directory %1 Q3LocalFsN bS_ %1Could not open %1 Q3LocalFsN Sv_U %1Could not read directory %1 Q3LocalFsN ydeNbv_U %1%Could not remove file or directory %1 Q3LocalFsN b %1 T}T N: %2Could not rename %1 to %2 Q3LocalFsN QQe %1Could not write %1 Q3LocalFs [NI... Customize... Q3MainWindowcRLine up Q3MainWindowdO\u(b7P\kbOperation stopped by the userQ3NetworkProtocolSmCancelQ3ProgressDialog^u(Apply Q3TabDialogSmCancel Q3TabDialog؋Defaults Q3TabDialog^.RHelp Q3TabDialogxnOK Q3TabDialog Y R6(&C)&Copy Q3TextEdit |4(&P)&Paste Q3TextEdit `bY (&R)&Redo Q3TextEdit dm(&U)&Undo Q3TextEditnzzClear Q3TextEdit RjR(&T)Cu&t Q3TextEdit bQh Select All Q3TextEditQsClose Q3TitleBarQszSCloses the window Q3TitleBarST+dO\zSvT}N0*Contains commands to manipulate the window Q3TitleBar f>y:zST y^vNST+~b[vcNFDisplays the name of the window and contains controls to manipulate it Q3TitleBar zSQh\OSMakes the window full screen Q3TitleBargY'SMaximize Q3TitleBarg\SMinimize Q3TitleBarbzSyR0YbMoves the window out of the way Q3TitleBarbNN*gY'SzS`bY N:fnr`&Puts a maximized window back to normal Q3TitleBarbNN*g\SzS`bY N:fnr`Puts a minimized back to normal Q3TitleBarTN `bY  Restore down Q3TitleBarTN `bY  Restore up Q3TitleBar|~System Q3TitleBar fY...More... Q3ToolBar (g*wv) (unknown) Q3UrlOperator*SO %1 N e/cY R6byReNbv_UIThe protocol `%1' does not support copying or moving files or directories Q3UrlOperatorSO %1 N e/cR^ev_U;The protocol `%1' does not support creating new directories Q3UrlOperatorSO %1 N e/cSeN0The protocol `%1' does not support getting files Q3UrlOperatorSO %1 N e/cRQv_U6The protocol `%1' does not support listing directories Q3UrlOperatorSO %1 N e/cN O eN0The protocol `%1' does not support putting files Q3UrlOperator"SO %1 N e/cydeNbv_U@The protocol `%1' does not support removing files or directories Q3UrlOperator$SO %1 N e/cT}T eNbv_U@The protocol `%1' does not support renaming files or directories Q3UrlOperatorSO %1 N e/c"The protocol `%1' is not supported Q3UrlOperator Sm(&C)&CancelQ3Wizard [b(&F)&FinishQ3Wizard ^.R(&H)&HelpQ3WizardN Nke(&N) >&Next >Q3Wizard< N Nke(&B)< &BackQ3Wizard cb~Connection refusedQAbstractSocketceConnection timed outQAbstractSocket N;g:g*b~R0Host not foundQAbstractSocket Q~N Network unreachableQAbstractSocketSocketdO\N e/c$Operation on socket is not supportedQAbstractSocketYWc[Wlg cSocket is not connectedQAbstractSocketYWc[WdO\eSocket operation timed outQAbstractSocket bQh(&S) &Select AllQAbstractSpinBox XR(&S)&Step upQAbstractSpinBox Q\(&D) Step &downQAbstractSpinBoxom;Activate QApplicationom;N*z ^vN;zS#Activates the program's main window QApplication0bgL %1 Qt %2 Sb~R0NQt %30,Executable '%1' requires Qt %2, found Qt %3. QApplicationN Q|[vQtIncompatible Qt Library Error QApplication Sm(&C)&Cancel QAxSelectCOM[a(&O) COM &Object: QAxSelectxn[OK QAxSelect bActiveXcNSelect ActiveX Control QAxSelect N-Check QCheckBoxRcbToggle QCheckBoxSm N-Uncheck QCheckBoxmRR0[NIr(&A)&Add to Custom Colors QColorDialogWg,r(&B) &Basic colors QColorDialog[NIr(&C)&Custom colors QColorDialog~r(&G)&Green: QColorDialog~r(&R)&Red: QColorDialogqT^(&S)&Sat: QColorDialogN^(&V)&Val: QColorDialogAlphaS(&A)A&lpha channel: QColorDialog݂r(&U)Bl&ue: QColorDialogr(&E)Hu&e: QColorDialog b阜r Select Color QColorDialogQsClose QComboBoxPGFalse QComboBoxbS_Open QComboBoxwTrue QComboBox%1ftok Y1%%1: ftok failedQCoreApplication%1.f/zzv%1: key is emptyQCoreApplication%1N R6 .%1: unable to make keyQCoreApplication N cNNRUnable to commit transaction QDB2DriverN cUnable to connect QDB2Driver N VnNRUnable to rollback transaction QDB2DriverN nRcNUnable to set autocommit QDB2Driver N ^.[SؑUnable to bind variable QDB2Result N bgLSUnable to execute statement QDB2ResultN S{,NN*Unable to fetch first QDB2ResultN SN NN*Unable to fetch next QDB2ResultN S֋_U%1Unable to fetch record %1 QDB2Result N QYSUnable to prepare statement QDB2ResultAMAM QDateTimeEditPMPM QDateTimeEditamam QDateTimeEditpmpm QDateTimeEdit QDialQDialQDialSliderHandle SliderHandleQDialSpeedoMeter SpeedoMeterQDial[bDoneQDialog f/NNH What's This?QDialog egOe9 Date Modified QDirModel|{WKind QDirModelT yName QDirModelY'\Size QDirModel|{WType QDirModelQsClose QDockWidgetcDock QDockWidgetmnRFloat QDockWidgetf\LessQDoubleSpinBoxfYMoreQDoubleSpinBox xn[(&O)&OK QErrorMessageQk!f>y:N*m`o(&S)&Show this message again QErrorMessage m`oDebug Message: QErrorMessage T} Fatal Error: QErrorMessagefTJWarning: QErrorMessageelR^ %1 Cannot create %1 for outputQFileelՏQe %1 Cannot open %1 for inputQFileelՏQCannot open for outputQFilevheN][XW(Destination file existsQFileQWWY1%Failure to write blockQFile.v_U%1 lg b~R00 h8[]~[kcxnv_UT 0K%1 Directory not found. Please verify the correct directory name was given. QFileDialog.eN%1 lg b~R00 h8[]~[kcxneNT 0A%1 File not found. Please verify the correct file name was given. QFileDialog %1]~[XW(0 O``fcb[NH-%1 already exists. Do you want to replace it? QFileDialog b(&C)&Choose QFileDialog R d(&D)&Delete QFileDialoge^eNY9(&N) &New Folder QFileDialog bS_(&O)&Open QFileDialogT}T (&R)&Rename QFileDialog O[X(&S)&Save QFileDialog( %1 f/QObv0 O`f/`R d[NH9'%1' is write protected. Do you want to delete it anyway? QFileDialogb@g eN (*) All Files (*) QFileDialogb@g eN (*.*)All Files (*.*) QFileDialogO`xnO``R d %1 !Are sure you want to delete '%1'? QFileDialogTBack QFileDialogN R dv_U0Could not delete directory. QFileDialog R^eeNY9Create New Folder QFileDialog~ƉV Detail View QFileDialogv_U Directories QFileDialogv_U Directory: QFileDialogqRVhDrive QFileDialogeNFile QFileDialogeNT y(&N) File &name: QFileDialog eN|{WFiles of type: QFileDialoggb~v_UFind Directory QFileDialogRMForward QFileDialogRhV List View QFileDialoggw Look in: QFileDialog bv{g: My Computer QFileDialog e^eNY9 New Folder QFileDialogbS_Open QFileDialogr6v_UParent Directory QFileDialog gvW0e Recent Places QFileDialogydRemove QFileDialogS[XN:Save As QFileDialogf>y: Show  QFileDialogf>y:eN(&H)Show &hidden files QFileDialogg*wvUnknown QFileDialog %1 GB%1 GBQFileSystemModel %1SC[W%1 KBQFileSystemModel %1 MB%1 MBQFileSystemModel %1 TB%1 TBQFileSystemModel%1[W%1 bytesQFileSystemModel`<b>T y %1 N Ou(0</b><p>Ou(SYNN*ST+f\[W{&bN T+g hp{&SvT y0oThe name "%1" can not be used.

Try using another name, with fewer characters or no punctuations marks.QFileSystemModel{g:ComputerQFileSystemModel egOe9 Date ModifiedQFileSystemModel eeHeNT Invalid filenameQFileSystemModel|{WKindQFileSystemModel bv{g: My ComputerQFileSystemModelT yNameQFileSystemModelY'\SizeQFileSystemModel|{WTypeQFileSystemModelNaAny QFontDatabase?bO/eArabic QFontDatabase N\_NRequest abortedQHttpSSL cbKY1%SSL handshake failedQHttpg RVh_^8W0QsNc%Server closed connection unexpectedlyQHttp g*wv Unknown errorQHttpb@c[vSOf/g*wvUnknown protocol specifiedQHttpvQ[^Wrong content lengthQHttpAuthentication requiredQHttpSocketEngineg*e6R0NtvHTTPT^(Did not receive HTTP response from proxyQHttpSocketEngineTHTTPNteSu#Error communicating with HTTP proxyQHttpSocketEnginegNtvlBQ/Error parsing authentication request from proxyQHttpSocketEngineNtceQs#Proxy connection closed prematurelyQHttpSocketEngineNtcb~Proxy connection refusedQHttpSocketEngine Ntb~ݏcProxy denied connectionQHttpSocketEngineNtg RVhce!Proxy server connection timed outQHttpSocketEngineg*b~R0Ntg RVhProxy server not foundQHttpSocketEngine N _YNRCould not start transaction QIBaseDriverbS_epcn^Error opening database QIBaseDriver N cNNRUnable to commit transaction QIBaseDriver N VnNRUnable to rollback transaction QIBaseDriver N RMSCould not allocate statement QIBaseResultN cϏQeS"Could not describe input statement QIBaseResult N cϏSCould not describe statement QIBaseResultN SN NyCould not fetch next item QIBaseResult N b~R0ep~Could not find array QIBaseResultN _R0ep~epcnCould not get array data QIBaseResultN _R0gO`oCould not get query info QIBaseResultN _R0SO`oCould not get statement info QIBaseResult N QYSCould not prepare statement QIBaseResult N _YNRCould not start transaction QIBaseResult N QsSUnable to close statement QIBaseResult N cNNRUnable to commit transaction QIBaseResultN R^BLOBUnable to create BLOB QIBaseResult N bgLgUnable to execute query QIBaseResultN bS_BLOBUnable to open BLOB QIBaseResultN SBLOBUnable to read BLOB QIBaseResultN QQeBLOBUnable to write BLOB QIBaseResultYN lg zzNNo space left on device QIODevicelg N*eNbv_UNo such file or directory QIODevice gCPb~Permission denied QIODeviceY*YbS_veNToo many open files QIODevice g*wv Unknown error QIODeviceMac OS XQelMac OS X input method QInputContextWindowsQelWindows input method QInputContextXIMXIM QInputContext XIMQelXIM input method QInputContext QeNN*P<Enter a value: QInputDialogelR}^%1%2Cannot load library %1: %2QLibrary"elՉg%2N-v{&S %1 %3$Cannot resolve symbol "%1" in %2: %3QLibraryelSx}^%1%2Cannot unload library %1: %2QLibraryN f \ %1 %2Could not mmap '%1': %2QLibraryN Smf \ %1 %2Could not unmap '%1': %2QLibrary %1 N-vcNepcnN S9M)Plugin verification data mismatch in '%1'QLibrary eN %1 N f/g eHvQtcN0'The file '%1' is not a valid Qt plugin.QLibrary@cN %1 Ou(NN Q|[vQt^0(%2.%3.%4) [%5]=The plugin '%1' uses incompatible Qt library. (%2.%3.%4) [%5]QLibraryJcN %1 Ou(NN Q|[vQt^0(N mTOu(^vrHg,TS^rHg,0)WThe plugin '%1' uses incompatible Qt library. (Cannot mix debug and release libraries.)QLibraryLcN %1 Ou(NN Q|[vQt^0g_vg^.f/ %2  _R0vStf/ %3 OThe plugin '%1' uses incompatible Qt library. Expected build key "%2", got "%3"QLibraryQqN^lg b~R00!The shared library was not found.QLibrary g*wv Unknown errorQLibrary Y R6(&C)&Copy QLineEdit |4(&P)&Paste QLineEdit `bY (&R)&Redo QLineEdit dm(&U)&Undo QLineEdit RjR(&T)Cu&t QLineEditR dDelete QLineEdit bQh Select All QLineEdit%1W0W@kcW(Ou(%1: Address in use QLocalServer%1: T y%1: Name error QLocalServer%1gCPb~%1: Permission denied QLocalServer%1g*w %2%1: Unknown error %2 QLocalServer%1c%1: Connection error QLocalSocket%1cb~%1: Connection refused QLocalSocket%1epcnbY*Y'%1: Datagram too large QLocalSocket%1eeHT y%1: Invalid name QLocalSocket%1z ]Qs%1: Remote closed QLocalSocket%1YWc[W%1: Socket access error QLocalSocket%1YWc[WdO\e%1: Socket operation timed out QLocalSocket%1YWc[WDn%1: Socket resource error QLocalSocket%1YWc[WdO\N e/c)%1: The socket operation is not supported QLocalSocket%1g*w%1: Unknown error QLocalSocket%1g*w %2%1: Unknown error %2 QLocalSocket N _YNRUnable to begin transaction QMYSQLDriver N cNNRUnable to commit transaction QMYSQLDriverN cUnable to connect QMYSQLDriverN bS_epcn^Unable to open database ' QMYSQLDriver N VnNRUnable to rollback transaction QMYSQLDriver N ~[YP<Unable to bind outvalues QMYSQLResult N ~[P<Unable to bind value QMYSQLResultN bgLN NN*gUnable to execute next query QMYSQLResult N bgLgUnable to execute query QMYSQLResult N bgLSUnable to execute statement QMYSQLResult N SepcnUnable to fetch data QMYSQLResult N QYSUnable to prepare statement QMYSQLResult N nSUnable to reset statement QMYSQLResultN [XPN NN*~gUnable to store next result QMYSQLResult N [XP~gUnable to store result QMYSQLResultN [XPS~g!Unable to store statement results QMYSQLResult (g*T}T v) (Untitled)QMdiArea%1 - [%2] %1 - [%2] QMdiSubWindow Qs(&C)&Close QMdiSubWindow yR(&M)&Move QMdiSubWindow `bY (&R)&Restore QMdiSubWindow Y'\(&S)&Size QMdiSubWindow - [%1]- [%1] QMdiSubWindowQsClose QMdiSubWindow^.RHelp QMdiSubWindowgY'S(&X) Ma&ximize QMdiSubWindowgY'SMaximize QMdiSubWindowSUMenu QMdiSubWindowg\S(&N) Mi&nimize QMdiSubWindowg\SMinimize QMdiSubWindow`bY Restore QMdiSubWindowTN `bY  Restore Down QMdiSubWindown=Shade QMdiSubWindow`;W(gRM(&T) Stay on &Top QMdiSubWindowSmn=Unshade QMdiSubWindowQsCloseQMenubgLExecuteQMenubS_OpenQMenuQsNQtAbout Qt QMessageBox^.RHelp QMessageBox ~Ƃ & &Hide Details... QMessageBoxxn[OK QMessageBox f>y:~Ƃ & &Show Details... QMessageBox b鏓Qel Select IMQMultiInputContextYQelRcbVhMultiple input method switcherQMultiInputContextPlugin*Ou(eg,zSNN N eSUvYQelRcbVhMMultiple input method switcher that uses the context menu of the text widgetsQMultiInputContextPlugin SNN*YWc[W]~kcW(vT,T NzS4Another socket is already listening on the same portQNativeSocketEngine2VW(N e/cIPv6e/cv^sSN Ou(IPv6YWc[W=Attempt to use IPv6 socket on a platform with no IPv6 supportQNativeSocketEngine cb~Connection refusedQNativeSocketEngineceConnection timed outQNativeSocketEngineN SѐY'vepcnbDatagram was too large to sendQNativeSocketEngine N;g:N Host unreachableQNativeSocketEngineeeHvYWc[WcϏ{&Invalid socket descriptorQNativeSocketEngineQ~ܕ Network errorQNativeSocketEngine Q~dO\eNetwork operation timed outQNativeSocketEngine Q~N Network unreachableQNativeSocketEngine[^YWc[WdO\Operation on non-socketQNativeSocketEngine Dn\=NOut of resourcesQNativeSocketEngine gCPb~Permission deniedQNativeSocketEngineSO|{WN e/cProtocol type not supportedQNativeSocketEngineN*W0W@N Su(The address is not availableQNativeSocketEngineN*W0W@ObNThe address is protectedQNativeSocketEngineT/u(vW0W@]~ψOu(#The bound address is already in useQNativeSocketEngine[NN*dO\Nt|{Wf/eeHv0,The proxy type is invalid for this operationQNativeSocketEnginezN;g:QsNN*c%The remote host closed the connectionQNativeSocketEngineN RYS^dYWc[W%Unable to initialize broadcast socketQNativeSocketEngineN RYS^;X^YWc[W(Unable to initialize non-blocking socketQNativeSocketEngineN ce6NN*m`oUnable to receive a messageQNativeSocketEngineN SѐNN*m`oUnable to send a messageQNativeSocketEngineN QQeUnable to writeQNativeSocketEngine g*wv Unknown errorQNativeSocketEngineN e/cvYWc[WdO\Unsupported socket operationQNativeSocketEnginebS_%1SuError opening %1QNetworkAccessCacheBackendelbS_ %1_f/NN*v_U#Cannot open %1: Path is a directoryQNetworkAccessFileBackendbS_ %1 %2Error opening %1: %2QNetworkAccessFileBackendS %1 %2Read error reading from %1: %2QNetworkAccessFileBackend kcW(bS_^g,W0eN %1 vlB%Request for opening non-local file %1QNetworkAccessFileBackendQQe %1 %2Write error writing to %1: %2QNetworkAccessFileBackendelՋS %1f/NN*v_UCannot open %1: is a directoryQNetworkAccessFtpBackendN } %1 e%2Error while downloading %1: %2QNetworkAccessFtpBackendN } %1 e%2Error while uploading %1: %2QNetworkAccessFtpBackendv{Qe %1 Y1%0Logging in to %1 failed: authentication requiredQNetworkAccessFtpBackendg*b~R0TvNtNo suitable proxy foundQNetworkAccessFtpBackendg*b~R0TvNtNo suitable proxy foundQNetworkAccessHttpBackend&N } %1  - g RVhVY %2)Error downloading %1 - server replied: %2 QNetworkReplySO %1 f/g*wvProtocol "%1" is unknown QNetworkReply dO\SmOperation canceledQNetworkReplyImpl N _YNRUnable to begin transaction QOCIDriver N cNNRUnable to commit transaction QOCIDriver N RYSUnable to initialize QOCIDriverN v{_UUnable to logon QOCIDriver N VnNRUnable to rollback transaction QOCIDriver N RMSUnable to alloc statement QOCIResultN ~[byYtbgLvR'Unable to bind column for batch execute QOCIResult N ~[P<Unable to bind value QOCIResultN bgLbyYtS!Unable to execute batch statement QOCIResult N bgLSUnable to execute statement QOCIResultN QeN NN*Unable to goto next QOCIResult N QYSUnable to prepare statement QOCIResult N cNNRUnable to commit transaction QODBCDriverN cUnable to connect QODBCDriver N c qRz ^N e/cb@g RCUnable to connect - Driver doesn't support all needed functionality QODBCDriverN ykbRcNUnable to disable autocommit QODBCDriverN bS_RcNUnable to enable autocommit QODBCDriver N VnNRUnable to rollback transaction QODBCDriverQODBCResult::reset: N b SQL_CURSOR_STATIC nN:S\^`'0hgO`vODBCqRz ^n0yQODBCResult::reset: Unable to set 'SQL_CURSOR_STATIC' as statement attribute. Please check your ODBC driver configuration QODBCResult N ^.[SؑUnable to bind variable QODBCResult N bgLSUnable to execute statement QODBCResultN SUnable to fetch QODBCResultN S{,NN*Unable to fetch first QODBCResultN SgTNN*Unable to fetch last QODBCResultN SN NN*Unable to fetch next QODBCResultN SN NN*Unable to fetch previous QODBCResult N QYSUnable to prepare statement QODBCResult[HomeQObjecteeHv URI%1Invalid URI: %1QObject g*c[N;g:T No host name givenQObjectW( %1 N N e/cvdO\Operation not supported on %1QObject SOe6R0NY'\N: 0 vS)Protocol error: packet of size 0 receivedQObjectS %1 %2Read error reading from %1: %2QObject*z N;g:eW0QsNW( %1 N vN*c3Remote host closed the connection prematurely on %1QObject%1 N vYWc[W%2Socket error on %1: %2QObjectQQe %1 %2Write error writing to %1: %2QObjectT yNameQPPDOptionsModelP<ValueQPPDOptionsModel N _YNRCould not begin transaction QPSQLDriver N cNNRCould not commit transaction QPSQLDriver N VnNRCould not rollback transaction QPSQLDriverN cUnable to connect QPSQLDriverN Unable to subscribe QPSQLDriver N SmUnable to unsubscribe QPSQLDriver N R^gUnable to create query QPSQLResult N QYSUnable to prepare statement QPSQLResultS|s (cm)Centimeters (cm)QPageSetupWidgetzOSFormQPageSetupWidget^Height:QPageSetupWidget[ (in) Inches (in)QPageSetupWidgetj*T LandscapeQPageSetupWidgetMarginsQPageSetupWidgetk|s (mm)Millimeters (mm)QPageSetupWidgeteT OrientationQPageSetupWidget ~_ Y'\ Page size:QPageSetupWidget~_ PaperQPageSetupWidget~_ n Paper source:QPageSetupWidget p (pt) Points (pt)QPageSetupWidget~TPortraitQPageSetupWidgetSTj*TReverse landscapeQPageSetupWidgetST~TReverse portraitQPageSetupWidget[^Width:QPageSetupWidgetN  bottom marginQPageSetupWidget]揹 left marginQPageSetupWidgetS󏹍 right marginQPageSetupWidgetN  top marginQPageSetupWidget Sm(&C)&CancelQPlatformTheme Qs(&C)&CloseQPlatformTheme T&(&N)&NoQPlatformTheme xn[(&O)&OKQPlatformTheme O[X(&S)&SaveQPlatformTheme f/(&Y)&YesQPlatformThemee>_AbortQPlatformTheme^u(ApplyQPlatformThemeSmCancelQPlatformThemeQsCloseQPlatformTheme N O[XQsClose without SavingQPlatformThemeb_DiscardQPlatformThemeN O[X Don't SaveQPlatformTheme^.RHelpQPlatformTheme_ueIgnoreQPlatformThemeQhT&(&O) N&o to AllQPlatformThemexn[OKQPlatformThemebS_OpenQPlatformThemenResetQPlatformTheme`bY ؋Restore DefaultsQPlatformTheme͋RetryQPlatformThemeO[XSaveQPlatformThemeO[XQhSave AllQPlatformThemeQhf/(&A) Yes to &AllQPlatformThemecNlg }Qe0The plugin was not loaded. QPluginLoader g*wv Unknown error QPluginLoader%1]~[XW(0 O``v[NH/%1 already exists. Do you want to overwrite it? QPrintDialog&%1f/v_U0 bNN*N T veNT 07%1 is a directory. Please choose a different file name. QPrintDialog y(&O) << &Options << QPrintDialog y(&O) >> &Options >> QPrintDialog bSSp(&P)&Print QPrintDialog <qt>O``v[NH</qt>%Do you want to overwrite it? QPrintDialogA0A0 QPrintDialog$A0 (841 x 1189 k|s)A0 (841 x 1189 mm) QPrintDialogA1A1 QPrintDialog"A1 (594 x 841 k|s)A1 (594 x 841 mm) QPrintDialogA2A2 QPrintDialog"A2 (420 x 594 k|s)A2 (420 x 594 mm) QPrintDialogA3A3 QPrintDialog"A3 (297 x 420 k|s)A3 (297 x 420 mm) QPrintDialogA4A4 QPrintDialog@A4 (210 x 297 k|s 8.26 x 11.7 [)%A4 (210 x 297 mm, 8.26 x 11.7 inches) QPrintDialogA5A5 QPrintDialog"A5 (148 x 210 k|s)A5 (148 x 210 mm) QPrintDialogA6A6 QPrintDialog"A6 (105 x 148 k|s)A6 (105 x 148 mm) QPrintDialogA7A7 QPrintDialog A7 (74 x 105 k|s)A7 (74 x 105 mm) QPrintDialogA8A8 QPrintDialogA8 (52 x 74 k|s)A8 (52 x 74 mm) QPrintDialogA9A9 QPrintDialogA9 (37 x 52 k|s)A9 (37 x 52 mm) QPrintDialog R+T %1 Aliases: %1 QPrintDialogB0B0 QPrintDialog&B0 (1000 x 1414 k|s)B0 (1000 x 1414 mm) QPrintDialogB1B1 QPrintDialog$B1 (707 x 1000 k|s)B1 (707 x 1000 mm) QPrintDialogB10B10 QPrintDialog B10 (31 x 44 k|s)B10 (31 x 44 mm) QPrintDialogB2B2 QPrintDialog"B2 (500 x 707 k|s)B2 (500 x 707 mm) QPrintDialogB3B3 QPrintDialog"B3 (353 x 500 k|s)B3 (353 x 500 mm) QPrintDialogB4B4 QPrintDialog"B4 (250 x 353 k|s)B4 (250 x 353 mm) QPrintDialogB5B5 QPrintDialog@B5 (176 x 250 k|s 6.93 x 9.84 [)%B5 (176 x 250 mm, 6.93 x 9.84 inches) QPrintDialogB6B6 QPrintDialog"B6 (125 x 176 k|s)B6 (125 x 176 mm) QPrintDialogB7B7 QPrintDialog B7 (88 x 125 k|s)B7 (88 x 125 mm) QPrintDialogB8B8 QPrintDialogB8 (62 x 88 k|s)B8 (62 x 88 mm) QPrintDialogB9B9 QPrintDialogB9 (44 x 62 k|s)B9 (44 x 62 mm) QPrintDialogC5EC5E QPrintDialog$C5E (163 x 229 k|s)C5E (163 x 229 mm) QPrintDialog[NICustom QPrintDialogDLEDLE QPrintDialog$DLE (110 x 220 k|s)DLE (110 x 220 mm) QPrintDialogQ{VeNf Executive QPrintDialogHExecutive (7.5 x 10 [ 191 x 254 k|s))Executive (7.5 x 10 inches, 191 x 254 mm) QPrintDialog*eN%1N SQ0 bNN*N T veNT 0=File %1 is not writable. Please choose a different file name. QPrintDialogeN[XW( File exists QPrintDialog[_~Folio QPrintDialog(Folio (210 x 330 k|s)Folio (210 x 330 mm) QPrintDialog^uLedger QPrintDialog*Ledger (432 x 279 k|s)Ledger (432 x 279 mm) QPrintDialogl_eNfLegal QPrintDialog@Legal (8.5 x 14 [ 216 x 356 k|s)%Legal (8.5 x 14 inches, 216 x 356 mm) QPrintDialogO~Letter QPrintDialogBLetter (8.5 x 11 [ 216 x 279 k|s)&Letter (8.5 x 11 inches, 216 x 279 mm) QPrintDialogg,W0eN Local file QPrintDialogxn[OK QPrintDialogbSSpPrint QPrintDialogbSSpR0eN & &Print To File ... QPrintDialogbSSpQh Print all QPrintDialogbSSpV Print range QPrintDialogbSSp bPrint selection QPrintDialogbSSpR0eN(PDF)Print to File (PDF) QPrintDialog"bSSpR0eN(Postscript)Print to File (Postscript) QPrintDialog\Wb~Tabloid QPrintDialog,Tabloid (279 x 432 k|s)Tabloid (279 x 432 mm) QPrintDialog" N vepPy:_SRMuShow facing pagesQPrintPreviewDialogf>y:b@g uviShow overview of all pagesQPrintPreviewDialogf>y:SUuShow single pageQPrintPreviewDialoge>Y'Zoom inQPrintPreviewDialog)\Zoom outQPrintPreviewDialog~AdvancedQPrintPropertiesWidgetzOSFormQPrintPropertiesWidgetuPageQPrintPropertiesWidgeth![CollateQPrintSettingsOutput_irColorQPrintSettingsOutput_irj!_ Color ModeQPrintSettingsOutputbCopiesQPrintSettingsOutputYNCopies:QPrintSettingsOutput N$RbSSpDuplex PrintingQPrintSettingsOutputzOSFormQPrintSettingsOutputpp^ GrayscaleQPrintSettingsOutputO Long sideQPrintSettingsOutputeNoneQPrintSettingsOutput yOptionsQPrintSettingsOutputQnOutput SettingsQPrintSettingsOutputuepN Pages fromQPrintSettingsOutputbSSpQh Print allQPrintSettingsOutputbSSpV Print rangeQPrintSettingsOutputSTReverseQPrintSettingsOutput b SelectionQPrintSettingsOutputwO Short sideQPrintSettingsOutputR0toQPrintSettingsOutputT y(&N)&Name: QPrintWidget...... QPrintWidgetzOSForm QPrintWidgetOMn Location: QPrintWidgetQeN(&F) Output &file: QPrintWidget \^`'(&R) P&roperties QPrintWidgetPreview QPrintWidgetbSSpg:Printer QPrintWidget|{WType: QPrintWidgetelbS_u(NSvQe[T,Could not open input redirection for readingQProcesselbS_u(NQQevQ[T-Could not open output redirection for writingQProcessNΏz N-SeSuError reading from processQProcessTz QQeeSuError writing to processQProcess z ]])nProcess crashedQProcess T/Rz Y1%Process failed to startQProcess z YteProcess operation timed outQProcessDn(forkY1%)%1!Resource error (fork failure): %1QProcessdmCancelQProgressDialogbS_Open QPushButton N-Check QRadioButtonv[W{&|{lbad char class syntaxQRegExpvmKlbad lookahead syntaxQRegExpvY lbad repetition syntaxQRegExpOu(NY1eHvryeHdisabled feature usedQRegExpeeHvQkR6epP<invalid octal valueQRegExp GR0QPR6met internal limitQRegExpb~N R0]R{&missing left delimQRegExp lg Suno error occurredQRegExp aYv~kbunexpected endQRegExpbS_epcn^Error to open databaseQSQLite2Driver N _YNRUnable to begin transactionQSQLite2Driver N cNNRUnable to commit transactionQSQLite2Driver N VnNRUnable to rollback TransactionQSQLite2Driver N bgLSUnable to execute statementQSQLite2Result N S~gUnable to fetch resultsQSQLite2ResultQsepcn^Error closing database QSQLiteDriverbS_epcn^Error opening database QSQLiteDriver N _YNRUnable to begin transaction QSQLiteDriver N cNNRUnable to commit transaction QSQLiteDriver N VnNRUnable to rollback transaction QSQLiteDriverlg gNo query QSQLiteResultSepepN S9MParameter count mismatch QSQLiteResult N ~[SepUnable to bind parameters QSQLiteResult N bgLSUnable to execute statement QSQLiteResult N SֈLUnable to fetch row QSQLiteResult N nSUnable to reset statement QSQLiteResult^Bottom QScrollBar]揹 Left edge QScrollBarTN cR Line down QScrollBarTN cRLine up QScrollBarN Nu Page down QScrollBar]Nu Page left QScrollBarSNu Page right QScrollBarN NuPage up QScrollBarOMnPosition QScrollBarS Right edge QScrollBarTN nR Scroll down QScrollBar nRR0ّ Scroll here QScrollBarT]nR Scroll left QScrollBarTSnR Scroll right QScrollBarTN nR Scroll up QScrollBarvTop QScrollBar%1]~[XW(%1: already exists QSharedMemory%1R^vY'\\N 0%1: create size is less then 0 QSharedMemory %1N [XW(%1: doesn't exists QSharedMemory%1ftok Y1%%1: ftok failed QSharedMemory%1eeHY'\%1: invalid size QSharedMemory%1: . %1: key error QSharedMemory%1.f/zzv%1: key is empty QSharedMemory%1lg DR%1: not attached QSharedMemory%1Dn\=N%1: out of resources QSharedMemory%1gCPb~%1: permission denied QSharedMemory%1Y'\gY1%%1: size query failed QSharedMemory%1|~ߘY'\PR6$%1: system-imposed size restrictions QSharedMemory%1elՕ[%1: unable to lock QSharedMemory%1N R6 .%1: unable to make key QSharedMemory%1elՋn[v.%1: unable to set key on lock QSharedMemory%1elSm[%1: unable to unlock QSharedMemory%1Unix .eNN [XW( %1: unix key file doesn't exists QSharedMemory%1g*w %2%1: unknown error %2 QSharedMemory++ QShortcutAltAlt QShortcutTBack QShortcutBackspace Backspace QShortcutBacktabBacktab QShortcutONX_: Bass Boost QShortcut\ON Bass Down QShortcutY'ONBass Up QShortcutT|SCall QShortcutCaps Lock Caps Lock QShortcutCapsLockCapsLock QShortcutN N e1Context1 QShortcutN N e2Context2 QShortcutN N e3Context3 QShortcutN N e4Context4 QShortcutCtrlCtrl QShortcutDelDel QShortcut DeleteDelete QShortcutDownDown QShortcutEndEnd QShortcut EnterEnter QShortcutEscEsc QShortcut EscapeEscape QShortcutF%1F%1 QShortcutgUr1v Favorites QShortcutlFlip QShortcutRMForward QShortcutcwHangup QShortcutHelpHelp QShortcutHomeHome QShortcutN;u Home Page QShortcutInsIns QShortcut InsertInsert QShortcut T/R (0) Launch (0) QShortcut T/R (1) Launch (1) QShortcut T/R (2) Launch (2) QShortcut T/R (3) Launch (3) QShortcut T/R (4) Launch (4) QShortcut T/R (5) Launch (5) QShortcut T/R (6) Launch (6) QShortcut T/R (7) Launch (7) QShortcut T/R (8) Launch (8) QShortcut T/R (9) Launch (9) QShortcut T/R (A) Launch (A) QShortcut T/R (B) Launch (B) QShortcut T/R (C) Launch (C) QShortcut T/R (D) Launch (D) QShortcut T/R (E) Launch (E) QShortcut T/R (F) Launch (F) QShortcutT/RN Launch Mail QShortcut T/RYZOS Launch Media QShortcutLeftLeft QShortcut N NN*YZOS Media Next QShortcut YZOSde> Media Play QShortcut N NN*YZOSMedia Previous QShortcut YZOS_U Media Record QShortcut YZOSP\kb Media Stop QShortcutMenuMenu QShortcutMetaMeta QShortcutT&No QShortcutNum LockNum Lock QShortcutNumLockNumLock QShortcutNumber Lock Number Lock QShortcut bS_URLOpen URL QShortcutPage Down Page Down QShortcutPage UpPage Up QShortcut PausePause QShortcut PgDownPgDown QShortcutPgUpPgUp QShortcut PrintPrint QShortcutPrint Screen Print Screen QShortcutR7eRefresh QShortcut ReturnReturn QShortcut RightRight QShortcutScroll Lock Scroll Lock QShortcutScrollLock ScrollLock QShortcutd}"Search QShortcut bSelect QShortcut ShiftShift QShortcutzzh<Space QShortcut{I_Standby QShortcutP\kbStop QShortcut SysReqSysReq QShortcutSystem RequestSystem Request QShortcutTabTab QShortcut\ؗ Treble Down QShortcutY'ؗ Treble Up QShortcutUpUp QShortcut\ Volume Down QShortcutY Volume Mute QShortcutY' Volume Up QShortcutf/Yes QShortcutN Nu Page downQSlider]Nu Page leftQSliderSNu Page rightQSliderN NuPage upQSliderOMnPositionQSliderN e/cvW0W@|{WAddress type not supportedQSocks5SocketEngine cN SOCKSv5g RVhQA(Connection not allowed by SOCKSv5 serverQSocks5SocketEngineNtceQs&Connection to proxy closed prematurelyQSocks5SocketEngine Ntb~ݏcConnection to proxy refusedQSocks5SocketEngine NtceConnection to proxy timed outQSocks5SocketEngine^8g RVhY1%General SOCKSv5 server failureQSocks5SocketEngine Q~dO\eNetwork operation timed outQSocks5SocketEngine NtY1%Proxy authentication failedQSocks5SocketEngineNtY1%: %1Proxy authentication failed: %1QSocks5SocketEngineNtN;g:g*b~R0Proxy host not foundQSocks5SocketEngineSOCKSrHg,5SOSOCKS version 5 protocol errorQSocks5SocketEngineN e/cvSOCKSv5T}NSOCKSv5 command not supportedQSocks5SocketEngine TTL]g TTL expiredQSocks5SocketEngine*g*wSOCKSv5Nt Nx 0x%1%Unknown SOCKSv5 proxy error code 0x%1QSocks5SocketEnginef\LessQSpinBoxfYMoreQSpinBoxSmCancelQSqlSm`vCancel your edits?QSqlxnConfirmQSqlR dDeleteQSqlR dga_UDelete this record?QSqlcQeInsertQSqlT&NoQSql O[X Save edits?QSqlfeUpdateQSqlf/YesQSqlN cOlg .vNf %1,Cannot provide a certificate with no key, %1 QSslSocketR^SSLN N e%1 Error creating SSL context (%1) QSslSocketR^SSLOݕ %1Error creating SSL session, %1 QSslSocketR^SSLOݕ%1Error creating SSL session: %1 QSslSocketSSLcbK%1Error during SSL handshake: %1 QSslSocketN }Qeg,W0Nf %1#Error loading local certificate, %1 QSslSocketN }Qeyg . %1Error loading private key, %1 QSslSocketSe%1Error while reading: %1 QSslSocketeeHbzzv}v[xRh%1 !Invalid or empty cipher list (%1) QSslSocketyg .N Qlg . %1/Private key does not certificate public key, %1 QSslSocketN QQeepcn%1Unable to write data: %1 QSslSocket%1]~[XW(%1: already existsQSystemSemaphore %1N [XW(%1: does not existQSystemSemaphore%1Dn\=N%1: out of resourcesQSystemSemaphore%1gCPb~%1: permission deniedQSystemSemaphore%1g*w %2%1: unknown error %2QSystemSemaphore N bS_cUnable to open connection QTDSDriverN Ou(epcn^Unable to use database QTDSDriverT]nR Scroll LeftQTabBarTSnR Scroll RightQTabBarsocketdO\N e/c$Operation on socket is not supported QTcpServer Y R6(&C)&Copy QTextControl |4(&P)&Paste QTextControl `bY (&R)&Redo QTextControl dm(&U)&Undo QTextControlY R6cOMn(&L)Copy &Link Location QTextControl RjR(&T)Cu&t QTextControlR dDelete QTextControl bQh Select All QTextControlbS_Open QToolButtonc N Press QToolButtonN*^sSN e/cIPv6#This platform does not support IPv6 QUdpSocket`bY Redo QUndoGroupdUndo QUndoGroup QUndoModel`bY Redo QUndoStackdUndo QUndoStackcQeUnicodecR6[W{& Insert Unicode control characterQUnicodeControlCharacterMenuLRE _YN]R0S]LQe$LRE Start of left-to-right embeddingQUnicodeControlCharacterMenuLRM N]R0ShLRM Left-to-right markQUnicodeControlCharacterMenuLRO _YN]TSv#LRO Start of left-to-right overrideQUnicodeControlCharacterMenuPDF _9QeTh<_PDF Pop directional formattingQUnicodeControlCharacterMenuRLE _YNST]]LQe$RLE Start of right-to-left embeddingQUnicodeControlCharacterMenuRLM NST]hRLM Right-to-left markQUnicodeControlCharacterMenuRLO _YNST]扆v#RLO Start of right-to-left overrideQUnicodeControlCharacterMenuZWJ [^cVhZWJ Zero width joinerQUnicodeControlCharacterMenuZWNJ [^^cVhZWNJ Zero width non-joinerQUnicodeControlCharacterMenuZWSP [^zzh<ZWSP Zero width spaceQUnicodeControlCharacterMenuelf>y: URLCannot show URL QWebFrameelf>y: MIMETYPECannot show mimetype QWebFrame eNN [XW(File does not exist QWebFrameVN:{VueetbSeNhbvR}&Frame load interruped by policy change QWebFrame lB;X^NRequest blocked QWebFrame lBSmNRequest cancelled QWebFrame%1 %2x%3 P} %1 (%2x%3 pixels)QWebPage %n N*eN %n file(s)QWebPage mRR0[WQxAdd To DictionaryQWebPagev HTTP lBBad HTTP requestQWebPage|OSBoldQWebPage^BottomQWebPagehglTbQCheck Grammar With SpellingQWebPagehgbQCheck SpellingQWebPageW(QeehgbQCheck Spelling While TypingQWebPage beN Choose FileQWebPagendgvd}"Clear recent searchesQWebPageY R6CopyQWebPageY R6VrG Copy ImageQWebPageY R6c Copy LinkQWebPageRjRCutQWebPage؋DefaultQWebPage R dR0SU\>Delete to the end of the wordQWebPage R dR0SU͙Delete to the start of the wordQWebPageeT DirectionQWebPage[WOSFontsQWebPageTGo BackQWebPageRM Go ForwardQWebPagebQTlHide Spelling and GrammarQWebPage_ueIgnoreQWebPage_ue Ignore Grammar context menu itemIgnoreQWebPagehgInspectQWebPageaY'R)OSItalicQWebPage"JavaScriptfTJ - %1JavaScript Alert - %1QWebPage"JavaScriptxn - %1JavaScript Confirm - %1QWebPage"JavaScriptcy: - %1JavaScript Prompt - %1QWebPageLTRLTRQWebPage]揹 Left edgeQWebPage W([WQxN-gb~Look Up In DictionaryQWebPageyRQIhR0WW\>'Move the cursor to the end of the blockQWebPageyRQIhR0eNg+\>*Move the cursor to the end of the documentQWebPageyRQIhR0L\>&Move the cursor to the end of the lineQWebPageyRQIhR0N NN*[W{&%Move the cursor to the next characterQWebPageyRQIhR0N NL Move the cursor to the next lineQWebPageyRQIhR0N NN*SU Move the cursor to the next wordQWebPageyRQIhR0N NN*[W{&)Move the cursor to the previous characterQWebPageyRQIhR0N NL$Move the cursor to the previous lineQWebPageyRQIhR0N NN*SU$Move the cursor to the previous wordQWebPageyRQIhR0WW)Move the cursor to the start of the blockQWebPageyRQIhR0eN_Y4,Move the cursor to the start of the documentQWebPageyRQIhR0L(Move the cursor to the start of the lineQWebPage lg b~R0smKNo Guesses FoundQWebPagelg eN bNo file selectedQWebPagelg gvd}"No recent searchesQWebPagebS_hFg Open FrameQWebPagebS_VrG Open ImageQWebPagebS_c Open LinkQWebPageW(ezSN-bS_Open in New WindowQWebPagen^OutlineQWebPageN Nu Page downQWebPage]Nu Page leftQWebPageSNu Page rightQWebPageN NuPage upQWebPage|4PasteQWebPageRTLRTLQWebPage gvd}"Recent searchesQWebPagee}QeReloadQWebPagenResetQWebPageS Right edgeQWebPageO[XVrG Save ImageQWebPageO[Xc... Save Link...QWebPageTN nR Scroll downQWebPage nRR0ّ Scroll hereQWebPageT]nR Scroll leftQWebPageTSnR Scroll rightQWebPageTN nR Scroll upQWebPaged}"QuSearch The WebQWebPage N-R0WW\>Select to the end of the blockQWebPage N-R0eN\>!Select to the end of the documentQWebPage N-R0L\>Select to the end of the lineQWebPage N-R0N NN*[W{&Select to the next characterQWebPage N-R0N NLSelect to the next lineQWebPage N-R0N NN*SUSelect to the next wordQWebPage N-R0N NN*[W{& Select to the previous characterQWebPage N-R0N NLSelect to the previous lineQWebPage N-R0N NN*SUSelect to the previous wordQWebPage N-R0WW Select to the start of the blockQWebPage N-R0eN#Select to the start of the documentQWebPage N-R0LSelect to the start of the lineQWebPagef>y:bQTlShow Spelling and GrammarQWebPagebQSpellingQWebPageP\kbStopQWebPagecNSubmitQWebPagecNQSubmit (input element) alt text for elements with no alt, title, or valueSubmitQWebPageeg,eTText DirectionQWebPage.f/NN*SNd}"v}"_0Qed}"vQs.[W3This is a searchable index. Enter search keywords: QWebPagevTopQWebPageN R~ UnderlineQWebPageg*wvUnknownQWebPageQuhgTX - %2Web Inspector - %2QWebPage f/NNH What's This?QWhatsThisAction**QWidget [b(&F)&FinishQWizard ^.R(&H)&HelpQWizardN Nke(&N)&NextQWizardN Nke(&N) >&Next >QWizard< N Nke(&B)< &BackQWizardSmCancelQWizardcNCommitQWizard~~ContinueQWizard[bDoneQWizardVGo BackQWizard^.RHelpQWizard%1 - [%2] %1 - [%2] QWorkspace Qs(&C)&Close QWorkspace yR(&M)&Move QWorkspace `bY (&R)&Restore QWorkspace Y'\(&S)&Size QWorkspace \U_(&U)&Unshade QWorkspaceQsClose QWorkspacegY'S(&X) Ma&ximize QWorkspaceg\S(&N) Mi&nimize QWorkspaceg\SMinimize QWorkspace`bY  Restore Down QWorkspace Sww(&A)Sh&ade QWorkspace`;W(gRM(&T) Stay on &Top QWorkspace2W(SXMLXfveP xXfbrzXfg_Yencoding declaration or standalone declaration expected while reading the XML declarationQXml W(NN*Y[OSveg,Xfg 3error in the text declaration of an external entityQXmlW(glvePSu$error occurred while parsing commentQXmlW(gQ[vePSu$error occurred while parsing contentQXml W(gehc|{W[NIvePSu5error occurred while parsing document type definitionQXmlW(gQC} vePSu$error occurred while parsing elementQXmlW(gS€vePSu&error occurred while parsing referenceQXmlu1m9QSverror triggered by consumerQXml*W(DTDN-N QAOu(Ygvu([OSS€;external parsed general entity reference not allowed in DTDQXml*W(\^`'Pg YQ[0!Extra content at end of document. QXmlStream^lvT}T zzXf0Illegal namespace declaration. QXmlStreameeHvXML[W{&0Invalid XML character. QXmlStreameeHvXMLT y0Invalid XML name. QXmlStreameeHvXMLrHg,[W{&N20Invalid XML version string. QXmlStreamW(XMLXfN-eeHv\^`'0%Invalid attribute in XML declaration. QXmlStreameeHv[W{&_u(0Invalid character reference. QXmlStream eeHvehc0Invalid document. QXmlStreameeHv[OSP<0Invalid entity value. QXmlStreameeHvYtcNT y0$Invalid processing instruction name. QXmlStreamW(Sep[OSXfN-g NDATA0&NDATA in parameter entity declaration. QXmlStream T}T zzv %1 RMlg Xf"Namespace prefix '%1' not declared QXmlStream_YhT~g_hN S9M0 Opening and ending tag mismatch. QXmlStreamehcev~g_0Premature end of document. QXmlStreamhmKR0]LYW[OS0Recursive entity detected. QXmlStream$W(\^`'P ^R0&Sequence ']]>' not allowed in content. QXmlStreamrzˏЈLSQAf/bT&0"Standalone accepts only yes or no. QXmlStream_Yg_vh0Start tag expected. QXmlStream"rzˏЈLO*\^`'_Ř{QsW(xNKT0?The standalone pseudo attribute must appear after the encoding. QXmlStreamaYv  Unexpected ' QXmlStream&W(Qlg heg,N-g aYv[W{& %1 0/Unexpected character '%1' in public id literal. QXmlStreamN e/cvXMLrHg,0Unsupported XML version. QXmlStream XMLXflg W(ehcv_YOMn0)XML declaration not at start of document. QXmlStream$%1 T %2 S9MNNLvY4T\>0,%1 and %2 match the start and end of a line. QtXmlPatternselՃS %1%1 cannot be retrieved QtXmlPatterns,%1ST+NW(lBx%2N-N QAvQkOMP<0E%1 contains octets which are disallowed in the requested encoding %2. QtXmlPatternsZ%1 f/NN*Y gB|{W0elbQR0Y gB|{W0Vkd bQR0OY %2 h7vS[P|{Wf/SNv0s%1 is an complex type. Casting to complex types is not possible. However, casting to atomic types such as %2 works. QtXmlPatterns%1 f/NN*eeHv %20%1 is an invalid %2 QtXmlPatterns0%1 f/kcRh_N-vNN*eeHh0g eHhN:?%1 is an invalid flag for regular expressions. Valid flags are: QtXmlPatterns$%1 f/NN*eeHvT}T zz URI0%1 is an invalid namespace URI. QtXmlPatterns(%1 f/kcRh_N-vNN*eeHj!_%2/%1 is an invalid regular expression pattern: %2 QtXmlPatterns %1N f/NN*Tlvj!gj!_T y0$%1 is an invalid template mode name. QtXmlPatterns%1 f/NN*g*wvehH|{W0%1 is an unknown schema type. QtXmlPatterns %1 f/N e/cvx0%1 is an unsupported encoding. QtXmlPatterns,%1 N f/NN*g eHv XML 1.0 [W{&0$%1 is not a valid XML 1.0 character. QtXmlPatterns %1N f/NN*YtcNvTlT y04%1 is not a valid name for a processing-instruction. QtXmlPatterns%1 N f/NN*g eHvep[WNI0"%1 is not a valid numeric literal. QtXmlPatterns@%1 N f/YtcNvg eHvhT y0[_Ř{f/P< %2 OY %30Z%1 is not a valid target name in a processing instruction. It must be a %2 value, e.g. %3. QtXmlPatterns"%1 N f/|{WN: %2 vg eHP<0#%1 is not a valid value of type %2. QtXmlPatterns%1 N f/Rvetep0$%1 is not a whole number of minutes. QtXmlPatterns(%1 N f/S[P|{W0SbQR0S[P|{W0C%1 is not an atomic type. Casting is only possible to atomic types. QtXmlPatterns8%1 N f/VQ\^`'Xf0laehH[Qery`'f/N e/cv0g%1 is not in the in-scope attribute declarations. Note that the schema import feature is not supported. QtXmlPatterns"%1 N f/|{WN: %2 vg eHP<0&%1 is not valid as a value of type %2. QtXmlPatterns%1 S9MNcbL{&%1 matches newline characters QtXmlPatterns>%1 _Ř{ %2 b %3 ߖ N W(fcb[W{&N2vg+\>0J%1 must be followed by %2 or %3, not at the end of the replacement string. QtXmlPatterns4%1 \ %n N*Sep0Vkd %2 f/eeHv0=%1 requires at least %n argument(s). %2 is therefore invalid. QtXmlPatterns6%1 gYSNg %n N*Sep0Vkd %2 f/eeHv09%1 takes at most %n argument(s). %2 is therefore invalid. QtXmlPatterns%1 u(N0%1 was called. QtXmlPatternslN ST+ %1A comment cannot contain %1 QtXmlPatternslN N %1 ~\>0A comment cannot end with a %1. QtXmlPatterns,GR0NNN*SQAW(XQueryN-Qsvg 0An %1-attribute must have a valid %2 as value, which %3 isn't. QtXmlPatterns*^&g P< %2 v %1 \^`']~XfN08An %1-attribute with value %2 has already been declared. QtXmlPatterns8T yN: %1 vSep]~ψXfN0kN*SepT y_Ř{U/N0UAn argument by name %1 has already been declared. Every argument name must be unique. QtXmlPatterns0NN*T yN: %1 v\^`']~QsW(N*QC} N-N0=An attribute by name %1 has already appeared on this element. QtXmlPatterns$NN*T yN: %1 v\^`']~ψR^01An attribute by name %1 has already been created. QtXmlPatternsRNN*\^`'pN f/NN*ehcpv[Pp0Vkd N*\^`' %1 b@W(OMnf/N Tv0dAn attribute node cannot be a child of a document node. Therefore, the attribute %1 is out of place. QtXmlPatterns%2_Ř{\SNN*[PQC} %103At least one %1 element must appear as child of %2. QtXmlPatterns"\NN*QC} %1QsW(%2NKRM0-At least one %1-element must occur before %2. QtXmlPatterns"\NN*QC} %1QsW(%2NKQ0-At least one %1-element must occur inside %2. QtXmlPatterns\g NN*~NTHs0'At least one component must be present. QtXmlPatterns*W(QC} %2v%1\^`'N-\c[NN*j!_0FAt least one mode must be specified in the %1-attribute on element %2. QtXmlPatterns0\NN*e~N_Ř{QsW(N* %1 uLPNKT0?At least one time component must appear after the %1-delimiter. QtXmlPatterns\^`'%1T%2_|kdNe0+Attribute %1 and %2 are mutually exclusive. QtXmlPatterns.\^`' %1 N N2LS VN:[QsW(gv\B0EAttribute %1 can't be serialized because it appears at the top level. QtXmlPatterns@\^`'%1N QsW(QC} %2N 0Sg %30%4ThQ\^`'f/QAv0]Attribute %1 cannot appear on the element %2. Allowed is %3, %4, and the standard attributes. QtXmlPatterns:\^`'%1N QsW(QC} %2N 0Sg %3ThQ\^`'f/QAv0YAttribute %1 cannot appear on the element %2. Allowed is %3, and the standard attributes. QtXmlPatterns:\^`'%1N QsW(QC} %2N 0Sg %3ThQ\^`'f/QAv0^Attribute %1 cannot appear on the element %2. Only %3 is allowed, and the standard attributes. QtXmlPatterns4\^`'%1N QsW(QC} %2N 0Sg hQ\^`'SNQs0VAttribute %1 cannot appear on the element %2. Only the standard attributes can appear. QtXmlPatterns\^`'%1vPelbQR0 %1 VN:[f/NN*ba|{W ^vNVkdelՈ[OS0fCasting to %1 is not possible because it is an abstract type, and can therefore never be instantiated. QtXmlPatternshmKR0sCircularity detected QtXmlPatterns %1 e[N %2 gf/eeHv0Day %1 is invalid for month %2. QtXmlPatterns*%1 ef/W( %2...%3 VNKYv0#Day %1 is outside the range %2..%3. QtXmlPatternsW(XSL-TNN- N Ou(%1t SOu(%2tb%3t0DIn an XSL-T pattern, axis %1 cannot be used, only axis %2 or %3 can. QtXmlPatterns.W(XSL-Th7_N- Qep%1N g {,N N*Sep0>In an XSL-T pattern, function %1 cannot have a third argument. QtXmlPatterns@W(XSL-Th7_N- Su(Qep%1T%2SNu(NS9M %3N SN0OIn an XSL-T pattern, only function %1 and %2, not %3, can be used for matching. QtXmlPatternsNW(XSL-Th7_N- Qep%1v{,NN*Sep_Ř{f/e[WbSؑS€ NOu(NS9M0yIn an XSL-T pattern, the first argument to function %1 must be a literal or a variable reference, when used for matching. QtXmlPatternsDW(XSL-Th7_N- Qep%1v{,NN*Sep_Ř{f/[W{&N2 NOu(NS9M0hIn an XSL-T pattern, the first argument to function %1 must be a string literal, when used for matching. QtXmlPatternsFW(N*fcb[W{&N2N- %1 Su(NlNI[g,b %2 N f/ %3MIn the replacement string, %1 can only be used to escape itself or %2, not %3 QtXmlPatternsDW(N*fcb[W{&N2N- %1 W(lg lNIveP_Ř{\NN*ep[Wߖ0VIn the replacement string, %1 must be followed by at least one digit when not escaped. QtXmlPatterns(etepdl(%1)d(%2)f/g*[NIv00Integer division (%1) by zero (%2) is undefined. QtXmlPatternsel~[R0N*RM %10+It is not possible to bind to the prefix %1 QtXmlPatternselN %1 bQR0 %20)It is not possible to cast from %1 to %2. QtXmlPatternsN Y XfRM %10*It is not possible to redeclare prefix %1. QtXmlPatterns\N S %10'It will not be possible to retrieve %1. QtXmlPatterns"N W(NOUQv[|{WpTmR\^`'0AIt's not possible to add attributes after any other kind of node. QtXmlPatterns,elN|{WN: %2 vP< %1 bQR0 %37It's not possible to cast the value %1 of type %2 to %3 QtXmlPatternsS9Mf/Y'\QN eOavMatches are case insensitive QtXmlPatterns,j!WW[QeN QsW(Qep0SؑT yXfNKRM0MModule imports must occur before function, variable, and option declarations. QtXmlPatterns(lBj!dl(%1)d(%2)f/g*[NIv00Modulus division (%1) by zero (%2) is undefined. QtXmlPatterns*%1 gf/W( %2...%3 VNKYv0%Month %1 is outside the range %2..%3. QtXmlPatternsDNN*|{WN: %1 vPOperator %1 cannot be used on atomic values of type %2 and %3. QtXmlPatterns$dO\{& %1 N u(N|{W %20&Operator %1 cannot be used on type %2. QtXmlPatterns@W(|{W %2 T %3 vS[PPu|{e n|{W_Ř{f/T N|{W b[_Ř{f/NN*[W{&N2|{W0|{W %2 f/N QAv0When casting to %1 or types derived from it, the source value must be of the same type, or it must be a string literal. Type %2 is not allowed. QtXmlPatterns:_SQep%1u(Nh7_S9Me Sep_Ř{f/SؑS€b[W{&N20vWhen function %1 is used for matching inside a pattern, the argument must be a variable reference or a string literal. QtXmlPatterns*zzv}[W{&ydN d^_S[NQsW([W{&|{N-OWhitespace characters are removed, except when they appear in character classes QtXmlPatternsrXSL-TQC} N-vXSL-T\^`'_Ř{e>W(zz(null)T}T zzN- N f/W(XSL-TT}T zzN- %1Stf/N*h7[P0iXSL-T attributes on XSL-T elements must be in the null namespace, not in the XSL-T namespace which %1 is. QtXmlPatterns*%1 ^tf/eeHv VN:^N %2 _Y0-Year %1 is invalid because it begins with %2. QtXmlPatternszzv}empty QtXmlPatternsxnRW0N exactly one QtXmlPatterns NbfY one or more QtXmlPatterns bfY zero or more QtXmlPatternsbN zero or one QtXmlPatterns]YMuted VolumeSlider %1% Volume: %1% VolumeSliderTreeLine/translations/treeline_de.qm0000644000175000017500000030161713715363644016626 0ustar dougdougwg/hDm4x;wybHBb^%D10^'^0Kt7T:8kNH5vH5DUVEc p%E!v$5vP-e.@0KFy5o37~6n&m*~(*y*X*X7*%X*T%*H!*0Y*5~+5t+UY+*+8a+++J+:[+į+Z/5!b5S4";0%T(M׽Jܷ!&݊  N4lQ[.E\  e k'U(I)yNJ-d9/c52t %OPՇǞUicڇf}4Kl^m@n5Brcs0uwUyY}X=*C2e?n]H^S=.':w9w96$6aD7ԯgc!zFu S3 f3Z|P؍[yً%wbMVC;(njﶰ~$}7seyy3"|c F )%v-%6n6n 6n@;Y U=>6kGpWR5C_ $c zg;_x?|7-ۇn_8%`{"Z uT?wi!e'[~&W~Үil<~)CNJu{hI  41A%g9 tZU^~NE#.E1S9-9@D-NQ~N$cQR XMk[ó9?aLdqaBf@g[i 5Klwpl$2oFszI}'~o. tQPq$H_b CˮPN*Z[y\ =§#³:G}֓M֓o^Z.aTbAUZ% x5v*T b.P<).&=\g(&+ 56aW>?d@B@{avFJ\8n`};f4gNehTUhYr`3I}: `KcZOd1G% 5Ec[N9J:YTR(N}..#kۊ+%H"D)aϑ=Į eT)לGשD-ؙR :vKgkHW]SO!mYmNst_d/-[}ѤcyA~n4U#CL#C&$*.,:.>7//~/a0I? In3{L RSeS"ZzZv]_Pb#e,fI`fȊmgl8!r^)sPDhjnd*ZNEI^Ti(/~|ϥ3KVx.s.R!@˃bΨ&;E7ie 7mv_'E>qh]p4 YIa.&k &t@L(m3H)d/C0ndV7O_8O_CdmP[R9L\SU/Uex]1sh^khkN7nV n5npct{}\ no`U+N5u / B? M[CǨ˛OXed"u#E)3R ^DfOxųY % <'Xq*Se,081cD1cD9H@EFy=LC{W0"Y[%x]!bbn#; wƾHjyw{0>HL "9S]cgc c!<HnjTz_#©VM*)tc9o3^ "eŻ%%%%xX7̇0;%USԤ?s޹߂SߗevB G+G^mҷ7   \~׭ \~ ;7 q#+[ ! 4E 5V :r F9 L> Mg f+>a k  8BL  $ ω \b b" % b* ld! u ߡwC B R V  ʵ, tfE n J  % gM VB cY s " .( q T|  W5 ,_$0a /3 4\ 5rp 7* = IT] Ty U Wܾ7N _҉' aI cF cT e uof u5g w5k ă\ 5C 1 3{^ :Z I I I I@ I& I I. IN Cop 8ro 9a # ? ,^ 6"<- 5O & 5 ^ I =? 3 t ˈ)= ĽN58 ;: Gc#? : :& G ;Km 5 e3 W ! 9 s .D Q t X KCB )M -8` .2' /. 2 T 50G 93v @(eY Aj KsE4 K' L.7 L9 W X [t_ pI3z rW\ x(  x(w {uX :3)( s# ݂ H  9 Z iTz < DJ (% ]l` Ka ` D(d c B ~2  t2 ٷa ߺS KGZ ~4 \E$j f ^V y~ 9 v S X EH% C +AP P %& #<5 Q* le aS '{ (ED * 2l" 7"+ 8Ǒn ;y9s = @|5 @I V9^ dMf g#2 n/Ѫ pk t$ t$ tr v" ! {B =I ?% eG hr M[ TJ Lb o ' C 4 e m) үQ vʉ bn`" D< U#  Y \ - D ]O '` .U 0uM 0 7y8 CB% G9. H.j Kz9j KC MU Ne ]`^d _ q dT i4 i$~X i$ zAx D 2_  ݃% |A =0 c( ˜S1 D}n Mr sL ~t > s :dx ^c"QssS(t(xf-/+ 0/^\0>^9= $Ej6 H;zL``YL}LyL^YEV6YbZrhji?k8lfc=p#Eqtt/CzoD=@^A//-ot_bzt:k{-`EmԕՁVYe/`9hj tpVC"`L;>Y Kc 9R(E68 qt^<=@-HNI@McU`Z8n: [K4e3Cem3MiFW>rլs$ww|*U}$7WC?y*A UӝSԲ88 m!]t`2Ii0mr-FMn$#HhD.%<u&$&h|Kɟ) ~~]= 3o:i&zo\dJ2n 3|56efuif&Abbrechen&Cancelcolorset&OK&OKcolorsetFarbschema Color Themecolorset"&Regel hinzufgen &Add New Rule conditional&Abbrechen&Cancel conditional&Schlieen&Close conditional&Lschen&Delete conditionalFilter &beenden &End Filter conditional&Filter&Filter conditional &Laden&Load conditional&OK&OK conditional&Regel lschen &Remove Rule conditionalS&peichern&Save conditional FalschFalse conditionalSuche &Nchsten Find &Next conditional"Suche &VorherigenFind &Previous conditional Name:Name: conditional@Keine bereinstimmungen gefunden!No conditional matches were found conditionalKnotentyp Node Type conditionalRegel {0}Rule {0} conditional&Gespeicherte Regeln Saved Rules conditionalWahrTrue conditional[Alle Typen] [All Types] conditionalundand conditionalenthltcontains conditionalendet mit ends with conditionaloderor conditionalbeginnt mit starts with conditional&Anwenden&Apply configdialog&Abbrechen&Cancel configdialog&Datentyp &Data Type configdialogTyp &lschen &Delete Type configdialog,Von &Original ableiten&Derive from original configdialog&Gleichung &Equation configdialog&Feld &konfigurieren &Field Config configdialog&Feldtyp &Field Type configdialog*Erweitert &ausblenden&Hide Advanced configdialogNach &unten &Move Down configdialog&Neues Feld... &New Field... configdialog&Neuer Typ... &New Type... configdialog&OK&OK configdialog&Prfix&Prefix configdialog&Zurcksetzen&Reset configdialog&Ergebnistyp &Result Type configdialog &Alles auswhlen &Select All configdialog&Erweitert &anzeigen&Show Advanced configdialog"&Sortierbedingung&Sort Criteria configdialog&&berschrift Format &Title Format configdialogN&Leere Zeilen zwischen KnotenhinzufgenAdd &blank lines between nodes configdialogFeld hinzufgen Add Field configdialogTyp hinzufgenAdd Type configdialogDDatentypen hinzufgen oder lschenAdd or Remove Data Types configdialog:&Aufzhlungspunkte hinzufgenAdd text bullet&s configdialog:&HTML Text im Format erlaubenAllow &HTML rich text in format configdialog0Arithmetische OperatorenArithmetic Operators configdialog$Automatische TypenAutomatic Types configdialog*Verfgbare &FeldlisteAvailable &Field List configdialog$Verfgbare &FelderAvailable &Fields configdialog&Bool'sches ErgebnisBoolean Result configdialognKann keinen Datentyp lschen, der noch in Benutzung ist+Cannot delete data type being used by nodes configdialog&Icon ndern Change &Icon configdialogAnzahl Kinder Child Count configdialogKind-VerweisChild Reference configdialog Kind Typ GrenzenChild Type Limits configdialog*Auswahl &zurcksetzen Clear &Select configdialog Typ &kopieren... Co&py Type... configdialogZAusgabe &Trenner fr Kombination && Kindliste+Combination && Child List Output &Separator configdialog(VergleichsoperatorenComparison Operators configdialog0Datentypen konfigurierenConfigure Data Types configdialogTyp kopieren Copy Type configdialog ZhlenCount configdialog.&Bedingte Typen anlegenCreate Co&nditional Types configdialog Datum Date Result configdialog:&Standardwert fr neue KnotenDefault &Value for New Nodes configdialog$Standard Kind &TypDefault Child &Type configdialog&Ausdruck definierenDefine Equation configdialogDMathematischen Ausdruck definierenDefine Math Field Equation configdialogFeld &lschen Dele&te Field configdialog0Abgeleitet von &BasistypDerived from &Generic Type configdialogBeschreibung Description configdialogRichtung Direction configdialogEditorhhe Editor Height configdialog>Name des neuen Feldes eingeben:Enter new field name: configdialogBGib den Namen des neuen Typs ein:Enter new type name: configdialog.Fehler in Gleichung: {}Equation error: {} configdialog^Fehler - Zirkulrer Verweis im berechnetem Feld2Error - circular reference in math field equations configdialog"Bewerte HTML TagsEvaluate &HTML tags configdialogZusatztext Extra Text configdialog &FeldF&ield configdialog&Feldliste F&ield List configdialogFeldField configdialog&Feldliste Field &List configdialogFeldverweisField References configdialog"Dateiinfo-VerweisFile Info Reference configdialog$Richtung &umkehrenFlip &Direction configdialog"Hilfe zum &Format Format &Help configdialogIconIcon configdialog.Mathematischer Ausdruck Math Equation configdialog*&Feldliste bearbeitenModify &Field List configdialog,&Bedingte Typen ndernModify Co&nditional Types configdialog,Nach &oben verschiebenMove &Up configdialog.Nach &unten verschieben Move Do&wn configdialog,Nach &oben verschiebenMove U&p configdialogNameName configdialogLeerNone configdialog,&Anzahl der TextzeilenNum&ber of text lines configdialogAnzahl Ergebnis Number Result configdialog"Anzahl der KinderNumber of Children configdialog$Typ des O&peratorsO&perator Type configdialog&AusgabeO&utput configdialog*Liste der Oper&atorenOper&ator List configdialogOperationen Operations configdialog6Verweise zu anderen FeldernOther Field References configdialog&AusgabeformatOut&put Format configdialog&AusgabeformatOutpu&t Format configdialogHTML Ausgabe Output HTML configdialogAusgabeoptionenOutput Options configdialogVater-VerweisParent Reference configdialogVerweis&typRefere&nce Type configdialogVerweis &StufeReference &Level configdialogVerweis &TypReference &Type configdialogVerweis&stufeReference Le&vel configdialog&Feld &umbenennen...Rena&me Field... configdialog$Typ &umbenennen...Rena&me Type... configdialogFeld umbenennen Rename Field configdialogTyp umbenennen Rename Type configdialog.Umbenennen von {} nach:Rename from {} to: configdialog$Verweis auf WurzelRoot Reference configdialog Nichts auswhlen Select &None configdialog.Verweis auf sich selbstSelf Reference configdialog,Datentyp Icon zuweisenSet Data Type Icon configdialog4Typ durch Bedingung setzenSet Types Conditionally configdialog"&Sortierfelder... Sort &Keys... configdialogSortierfeldSort Key configdialogSortierfelderSort Key Fields configdialog&SuffixSuffi&x configdialog&Typliste T&ype List configdialogText OperatorenText Operators configdialogText Ergebnis Text Result configdialogXDie folgenden Zeichen sind nicht erlaubt: {},The following characters are not allowed: {} configdialog:Der Name kann nicht leer seinThe name cannot be empty configdialogRDer Name darf keine Leerzeichen enthaltenThe name cannot contain spaces configdialogLDer Name kann nicht mit "xml" beginnen The name cannot start with "xml" configdialogVDer Name muss mit einem Buchstaben beginnen!The name must start with a letter configdialogDDer Name ist bereits in VerwendungThe name was already used configdialogUhrzeit Time Result configdialog$Typ &Konfiguration Typ&e Config configdialogTypType configdialogJEine Tabelle fr &Feldwerte verwendenUse a table for field &data configdialog,[Alle Typen verfgbar][All Types Available] configdialog [Leer][None] configdialogAbsolutwertabsolute value configdialoghinzufgenadd configdialogArcus Cosinus arc cosine configdialogArcus Sinusarc sine configdialogArcus Tangens arc tangent configdialogDurchschnittaverage configdialog(Logarithmus Basis 10base-10 logarithm configdialog*Text aneinanderhngenconcatenate text configdialogJText in Kleinbuchsrtaben konvertierenconvert text to lower case configdialogFText in Grobuchstaben konvertierenconvert text to upper case configdialog Cosinus Bogenmacosine of radians configdialog$Grad nach Bogenmadegrees to radians configdialogdividierendivide configdialogIst gleichequal to configdialogFakultt factorial configdialogFliesskommazahlfloating point configdialog*dividieren mit runden floor divide configdialogvorwrtsforward configdialog>>fwd configdialogIst grer greater than configdialog,Ist grer oder gleichgreater than or equal to configdialogHhere Ganzzahlhigher integer configdialogxErsetze in 1. Argument das 2. Argument durch das 3. Argument(in 1st arg, replace 2nd arg with 3rd arg configdialogTText verbinden mit 1. Argument als Trenner$join text using 1st arg as separator configdialogIst kleiner less than configdialog.Ist kleiner oder gleichless than or equal to configdialogLogisches Und logical and configdialogLogisches Oder logical or configdialog&Niedrigere Ganzzahl lower integer configdialogMaximummaximum configdialogMinimumminimum configdialog modulomodulus configdialogmultiplizierenmultiply configdialogEulersche Zahlnatural log constant configdialog.Natrlicher Logarithmusnatural logarithm configdialog Ist nicht gleich not equal to configdialogPI pi constant configdialogexponierenpower configdialog$Bogenma nach Gradradians to degrees configdialog<<rev configdialogrckwrtsreverse configdialog(Runden auf n Stellenround to num digits configdialogSinus Bogenmasine of radians configdialogQuadratwurzel square root configdialogabziehensubtract configdialogsummieren sum of items configdialog Tangens Bogenmatangent of radians configdialogPWahr, wenn 1.Argument 2.Argument enthlt%true if 1st text arg contains 2nd arg configdialogZWahr, wenn 1. Argument mit 2. Argument endet &true if 1st text arg ends with 2nd arg configdialog\Wahr, wenn 1. Argument mit 2. Argument beginnt(true if 1st text arg starts with 2nd arg configdialogJWahrer Wert, Bedingung, Falscher Wert"true value, condition, false value configdialog.Abgeschnittene Ganzzahltruncated integer configdialogDatei &suchen&Browse for File dataeditors&Abbrechen&Cancel dataeditors&Gehe zu Ziel &Go to Target dataeditors&OK&OK dataeditors&Verweis ffnen &Open Link dataeditors&ffne Bild &Open Picture dataeditors6Klicke Verweis Ziel im Baum(Click link target in tree) dataeditorsAbsolutAbsolute dataeditorsAdresseAddress dataeditorsVerweis lschen Clear &Link dataeditorsAnzeigename Display Name dataeditors Externer Verweis External Link dataeditors$Verzeichnis AngabeFile Path Type dataeditors Interner Verweis Internal Link dataeditors&&Verzeichnis ffnen Open &Folder dataeditors Verweis auf Bild Picture Link dataeditorsRelativRelative dataeditors SchemaScheme dataeditors"Auf &heute setzen Set to &Now dataeditorsHeutiges &Datum Today's &Date dataeditorsDVerweis zu externer TreeLine DateiTreeLine - External Link File dataeditors$TreeLine BilddateiTreeLine - Picture File dataeditorsVerweislink dataeditors&Spalten&Columnsexportsx&Komma-getrennte Tabelle (CSV) der Nachkommen (Nummern Grad);&Comma delimited (CSV) table of descendants (level numbers)exports&Gesamter Baum &Entire treeexports &HTML&HTMLexports:&HTML formatierte Lesezeichen&HTML format bookmarksexports8einschlielich Wurzel Knoten&Include root nodesexports&ODF Struktur &ODF Outlineexports:Altes TreeLine Format (2.0.x)&Old TreeLine (2.0.x)exports2&Nur geffnete Kindknoten&Only open node childrenexports(&Einzelne HTML Seite&Single HTML pageexports2&Titelzeile mit Tabulator&Tabbed title textexports &Text&Textexports"TreeLine Teilbaum&TreeLine SubtreeexportsT&Unformatierte Ausgabe des gesamten Textes&Unformatted output of all textexports:&XBEL formatierte Lesezeichen&XBEL format bookmarksexports &XML (generisch)&XML (generic)exports&Lesezeichen Book&marksexportsLesezeichen Bookmarksexports4Ausgabe Unterformat whlenChoose export format subtypeexports(Ausgabeformat whlenChoose export format typeexports,Ausgabeoptionen whlenChoose export optionsexportstKomma-&getrennte Tabelle (CSV) der Kinder (einzelner Grad)7Comma &delimited (CSV) table of children (single level)exportsFehler - Kann nicht auf nicht gespeicherte TreeLine Datei verweisen. Speichere die Datei und versuche es erneut.FError - cannot link to unsaved TreeLine file. Save the file and retry.exportsFehler - Export Vorlage nicht gefunden. berprfe deine TreeLine Installation.JError - export template files not found. Check your TreeLine installation.exportsDatei Export File Exportexports@&Kopf- und Fuzeile einschlieenInclude &print header && footerexportsLive Baum Ansicht, verlinkt mit der TreeLine Datei (fr Web Server)8Live tree view, linked to TreeLine file (for web server)exportshLive Baum Ansicht, eine Datei (Daten eingeschlossen)+Live tree view, single file (embedded data)exports6Mehrere HTML &DatentabellenMultiple HTML &data tablesexportsTMehrere HTML &Seiten mit Navigationsleiste)Multiple HTML &pages with navigation paneexportsVSie mssen Knoten vor dem Export auswhlen.!Must select nodes prior to exportexports:&Stufen der NavigationsleisteNavigation pane &levelsexportsAndere Optionen Other Optionsexports ElternParentexports,&Ausgewhlte TeilbumeSelected &branchesexports&Ausgewhlte &KnotenSelected &nodesexportsTEinzelne &HTML Seite mit Navigationsleiste&Single &HTML page with navigation paneexportsfTab-&getrennte Tabelle der Kinder (&einzelner Grad)0Tab &delimited table of children (&single level)exportsTree&Line Tree&LineexportsJTreeLine - Exportiere Generisches XMLTreeLine - Export Generic XMLexports4TreeLine - Exportiere HTMLTreeLine - Export HTMLexportsLTreeLine - Exportiere HTML Lesezeichen TreeLine - Export HTML Bookmarksexports<TreeLine - Exportiere ODF TextTreeLine - Export ODF Textexports4TreeLine - Exportiere TextTreeLine - Export Plain TextexportsDTreeLine - Exportiere Text TabelleTreeLine - Export Text TablesexportsNTreeLine - Exportiere TextberschriftenTreeLine - Export Text TitlesexportsNTreeLine - Exportiere TreeLine Teilbaum"TreeLine - Export TreeLine SubtreeexportsLTreeLine - Exportiere XBEL Lesezeichen TreeLine - Export XBEL BookmarksexportsWarnung - kein relativer Pfad von "{0}" nach "{1}". Weitermachen mit absolutem Pfad?LWarning - no relative path from "{0}" to "{1}". Continue with absolute path?exports&Was wird exportiertWhat to Exportexports"." Zeichen .."." Character .. fieldformat"/" Zeichen //"/" Character // fieldformat00 oder 1 Wiederholung ?0 Or 1 Repetitions ? fieldformat:0 oder mehr Wiederholungen *0 Or More Repetitions * fieldformat:1 oder mehr Wiederholungen +1 Or More Repetitions + fieldformat AM/PMAM/PM %p fieldformat*Beliebiges Zeichen .Any Character . fieldformat Auto Auswahlfeld AutoChoice fieldformat*Auto-KombinationsfeldAutoCombination fieldformatBooleanBoolean fieldformat Grobuchstabe ACapital Letter A fieldformat2Groe rmische Ziffer ICapital Roman Numeral I fieldformatAuswahlfeldChoice fieldformat Kombinationsfeld Combination fieldformat"Komma Trenner \,Comma Separator \, fieldformat DatumDate fieldformatDatum/ZeitDateTime fieldformat,Tag (1 oder 2 Ziffern)Day (1 or 2 digits) %-d fieldformatTag (2 Ziffern)Day (2 digits) %d fieldformat4Tag des Jahres (1 bis 366)Day of year (1 to 366) %-j fieldformatDezimalkomma ,Decimal Comma , fieldformatDezimalpunkt .Decimal Point . fieldformat*Anzahl der NachkommenDescendantCount fieldformatNZiffer oder Leertaste (Extern) <space>!Digit or Space (external)  fieldformat"Punkt Trenner \.Dot Separator \. fieldformatTexteende $ End of Text $ fieldformat6Escape fr Sonderzeichen \Escape a Special Character \ fieldformat"Beispiel 1/2/3/4Example 1/2/3/4 fieldformat$Exponent (Gro) EExponent (capital) E fieldformat&Exponent (Klein) eExponent (small) e fieldformat Externer Verweis ExternalLink fieldformat>Stunde (0-23, 1 oder 2 Ziffern)Hour (0-23, 1 or 2 digits) %-H fieldformat2Stunde (00-23, 2 Ziffern)Hour (00-23, 2 digits) %H fieldformat2Stunde (01-12, 2 Ziffern)Hour (01-12, 2 digits) %I fieldformat>Stunde (1-12, 1 oder 2 Ziffern)Hour (1-12, 1 or 2 digits) %-I fieldformatHtmlTextHtmlText fieldformat Interner Verweis InternalLink fieldformat Stufentrenner /Level Separator / fieldformat*Kleinbuchstabe [a-z]Lower Case Letters [a-z] fieldformatMathematischMath fieldformat0Microsekunde (6 Ziffern)Microseconds (6 digits) %f fieldformat2Minute (1 oder 2 Ziffern)Minute (1 or 2 digits) %-M fieldformat$Minute (2 Ziffern)Minute (2 digits) %M fieldformat0Monat (1 oder 2 Ziffern)Month (1 or 2 digits) %-m fieldformat"Monat (2 Ziffern)Month (2 digits) %m fieldformatMonat AbkrzungMonth Abbreviation %b fieldformatMonat Name Month Name %B fieldformat(Keine Ziffer [^0-9]Not a Number [^0-9] fieldformat HeuteNow fieldformat NummerNumber fieldformatZiffer 1Number 1 fieldformat Nummer Numbering fieldformatEinzeiler OneLineText fieldformat&Optionale Ziffer #Optional Digit # fieldformat0Optionales Vorzeichen -Optional Sign - fieldformatOder |Or | fieldformatLGliederungsbeispiel I../A../1../a)/i)!Outline Example I../A../1../a)/i) fieldformatBildPicture fieldformat$Regulrer AusdruckRegularExpression fieldformat.Erforderliche Ziffer 0Required Digit 0 fieldformat8Erforderliches Vorzeichen +Required Sign + fieldformat4Sekunde (1 oder 2 Ziffern)Second (1 or 2 digits) %-S fieldformat&Sekunde (2 Ziffern)Second (2 digits) %S fieldformat8berschrift Beispiel 1.1.1.1Section Example 1.1.1.1 fieldformat$Sektionstrenner .Section Separator . fieldformatTrenner / Separator / fieldformat0Menge von Ziffern [0-9]Set of Numbers [0-9] fieldformat"Kleinbuchstabe aSmall Letter a fieldformat2Kleine rmische Ziffer iSmall Roman Numeral i fieldformatFLeertaste Trenner (Intern) <space>"Space Separator (internal)  fieldformat4Text, Leerzeichen getrennt SpacedText fieldformatW/FT/F fieldformatTextText fieldformatZeitTime fieldformat(Grobuchstabe [A-Z]Upper Case Letters [A-Z] fieldformat.Wochennummer (0 bis 53)Week Number (0 to 53) %-U fieldformat&Wochentag AbkrzungWeekday Abbreviation %a fieldformatWochentag NameWeekday Name %A fieldformatJ/NY/N fieldformat Jahr (2 Ziffern)Year (2 digits) %y fieldformat Jahr (4 Ziffern)Year (4 digits) %Y fieldformatwahr/falsch true/false fieldformatja/neinyes/no fieldformat falschfalse genbooleanneinno genbooleanwahrtrue genbooleanjayes genbooleanAlle Dateien All Files globalref*Alle Treeline DateienAll TreeLine Files globalref4CSV Datei (Komma-getrennt)CSV (Comma Delimited) Files globalrefHTML Dateien HTML Files globalrefODF DateienODF Text Files globalref*Alte Treeline DateienOld TreeLine Files globalrefPDF Dateien PDF Files globalrefText Dateien Text Files globalref TreeLine DateienTreeLine Files globalref<TreeLine Dateien (komprimiert)TreeLine Files - Compressed globalref@TreeLine Dateien (verschlsselt)TreeLine Files - Encrypted globalrefTreepad Dateien Treepad Files globalrefXML Dateien XML Files globalrefSuchen:  Find: helpview&Zurck&Backhelpview&Vorwrts&Forwardhelpview(&Eigenes Verzeichnis&Homehelpview &Vorwrts Suchen Find &Nexthelpview"&Rckwrts SuchenFind &Previoushelpview&Text nicht gefundenText string not foundhelpviewWerkzeugeToolshelpviewn"{0}" ist keine TreeLine Datei. Importfilter verwenden?:"{0}" is not a valid TreeLine file. Use an import filter?importsNGenerisches &XML (Keine TreeLine Datei) &Generic XML (non-TreeLine file)importsD&HTML Lesezeichen (Mozilla Format) &HTML bookmarks (Mozilla Format)importsb&Mit Tabulator eingerckter Text, Knoten je Zeile%&Tab indented text, one node per lineimportsF&XML Lesezeichendatei (XBEL-Format)&XML bookmarks (XBEL format)importsLESEZEICHENBOOKMARKimportsDFehler im CSV Format bei Zeile {0}Bad CSV format on Line {0}importsLesezeichen Bookmarksimports0Import Methode auswhlenChoose Import MethodimportsKo&mma-getrennter (CSV) Text Tabelle mit Stufe Spalte && Kopfzeile ReiheACo&mma delimited (CSV) text table with level column && header rowimportsdKo&mma-getrennter (CSV) Text Tabelle mit Kopfzeile1Comma delimited (CSV) text table &with header rowimportsFFehler - Kann Datei {0} nicht lesenError - could not read file {0}importsBFehler - ungltiges Format in {0}Error - improper format in {0}importsVERZEICHNISFOLDERimportsImport Datei Import FileimportsUngltige Datei Invalid Fileimports@Falsche Ebene Nummer on line {0} Invalid level number on line {0}imports6Fehlerhafte Ebenen StrukturInvalid level structureimportsVerweisLinkimportsFAlte Tree&Line Datei (1.x oder 2.x)Old Tree&Line File (1.x or 2.x)imports>Open &Document (ODF) GliederungOpen &Document (ODF) outlineimports AndereOtherimportsHText &Blcke (Beendet mit Leerzeile)-Plain text ¶graphs (blank line delimited)importsZNur Text, ein &Knoten pro Zeile (CR getrennt)-Plain text, one &node per line (CR delimited)importsTRENNER SEPARATORimportsTABELLETABLEimportsNTabulator getrennter Text mit Kopfzeile)Tab delimited text table with header &rowimportsTextTextimportsDZu viele Eintrge in der Zeile {0}Too many entries on Line {0}imports.TreeLine - Import DateiTreeLine - Import Fileimports2Treepad &Datei (nur Text)Treepad &file (text nodes only)imports|Verweise auf Kinder mssen in einer Funktion kombiniert werden/Child references must be combined in a functionmatheval,Ungltige Zeichen "{}"Illegal "{}" charactersmatheval.Ungltige Funktion: {0}Illegal function present: {0}mathevalNUngltiger Objekttyp oder Operator: {0}$Illegal object type or operator: {0}matheval8Ungltige Syntax in AusdruckIllegal syntax in equationmatheval&Anwenden&Apply miscdialogs&Abbrechen&Cancel miscdialogs&Schlieen&Close miscdialogsFilter &Ende &End Filter miscdialogs&Gesamter Baum &Entire tree miscdialogs&Filter&Filter miscdialogsSuche &Nchster &Find Next miscdialogs&Vorwrts&Forward miscdialogs&Ignorieren&Ignore and skip miscdialogs&Schlsselworte &Key words miscdialogs&Knotentyp &Node Type miscdialogs&OK&OK miscdialogs<&Vordefinierte Schlsselfelder&Predefined Key Fields miscdialogs&&Regulrer Ausdruck&Regular expression miscdialogs&Ersetzen&Replace miscdialogsb&Nummerierung fr nchste Geschwister neu starten"&Restart numbers for next siblings miscdialogs4&Standard wiederherstellen&Restore Defaults miscdialogs&Rckwrts&Reverse miscdialogs&Suchtext &Search Text miscdialogs&&Kinder der Auswahl&Selection's children miscdialogsNur &Titel &Titles only miscdialogs&Symbolleisten &Toolbars miscdialogs@&Leere Felder als Null behandeln&Treat blank fields as zeros miscdialogs<&Standard Schriftart verwenden&Use app default font miscdialogs:Datei&komprimierung verwenden&Use file compression miscdialogs8&System Schriftart verwenden&Use system default font miscdialogs--Trenner-- --Separator-- miscdialogsEin schwerer Fehler ist aufgetreten. TreeLine kann sich in einem instabilen Zustand befinden. Empfehlung, die Datei unter einem anderen Namen zu speichern und TreeLine neu zu starten. Das Debug Info unterhalb kann kopiert werden und per Mail an doug101@bellz.org geschickt werden zusammen mit Erluterungen, unter welchen Umstnden, der Fehler auftrat.A serious error has occurred. TreeLine could be in an unstable state. Recommend saving any file changes under another filename and restart TreeLine. The debugging info shown below can be copied and emailed to doug101@bellz.org along with an explanation of the circumstances.  miscdialogs,&Verfgbare FunktionenA&vailable Commands miscdialogs Jedes &Vorkommen Any &match miscdialogs$StandardschriftartApp Default Font miscdialogsLsche &Taste Clear &Key miscdialogs*Schriftarten AnpassenCustomize Fonts miscdialogs6Symbolleisten konfigurierenCustomize Toolbars miscdialogs DatenData miscdialogsDaten Men Data Menu miscdialogs6Standard - Einzeiliger TextDefault - Single Line Text miscdialogsBearbeitenEdit miscdialogsBearbeiten Men Edit Menu miscdialogs*Schriftart fr EditorEditor View Font miscdialogsDPasswort der verschlsselten DateiEncrypted File Password miscdialogsLFehler - Ungltiger regulrer Ausdruck"Error - invalid regular expression miscdialogsBFehler - Ersetzung nicht mglich Error - replacement failed miscdialogsGanzer &Satz F&ull phrase miscdialogs FelderFields miscdialogs DateiFile miscdialogsDatei Men File Menu miscdialogs&Datei EigenschaftenFile Properties miscdialogs"Datei Speicherung File Storage miscdialogs FilterFilter miscdialogs SuchenFind miscdialogsSuche &Nchster Find &Next miscdialogs Suche &VorherigeFind &Previous miscdialogs&Suchen und ErsetzenFind and Replace miscdialogs FormatFormat miscdialogsFormat Men Format Menu miscdialogsGesamte &Daten Full &data miscdialogsGanze &Worte Full &words miscdialogs\Behandlung von Knoten ohne Nummerierungsfelder'Handling Nodes without Numbering Fields miscdialogs HilfeHelp miscdialogsHilfe Men Help Menu miscdialogs Wie wird gesucht How to Search miscdialogsJKnoten der ersten Ebene einschliessenInclude top-level nodes miscdialogs&Ganze WorteKey full &words miscdialogs0Taste {0} bereits belegtKey {0} is already used miscdialogsTastaturkrzelKeyboard Shortcuts miscdialogsJSprachcode oder Wrterbuch (optional)&Language code or dictionary (optional) miscdialogsGroe Icons Large Icons miscdialogs&Mathematisches Feld Math Fields miscdialogs.Nach &unten verschieben Move &Down miscdialogs,Nach &oben verschiebenMove &Up miscdialogs&Felder N&ode Fields miscdialogsKein MenNo menu miscdialogsDKeine Nummerierungsfelder gefunden,No numbering fields were found in data types miscdialogs KnotenNode miscdialogsKnoten&titel Node &Titles miscdialogsKnoten Men Node Menu miscdialogs,Schriftart fr AusgabeOutput View Font miscdialogs&Regulrer &AusdruckRe&gular expression miscdialogs*Passwort wiederholen:Re-Type Password: miscdialogs>Passworte stimmen nicht bereinRe-typed password did not match miscdialogsBPasswort fr diese Sitzung merken%Remember password during this session miscdialogs&Alle Ersetzen Replace &All miscdialogs&{0} Stellen ersetztReplaced {0} matches miscdialogsErsetzungs&textReplacement &Text miscdialogs,Nummerierung &umkehrenReserve &numbers miscdialogsWurzelelement Root Node miscdialogs:Suchtext "{0}" nicht gefundenSearch string "{0}" not found miscdialogs:Suchtext "{0}" nicht gefundenSearch text "{0}" not found miscdialogs,Ausgewhlte &TeilbumeSelected &branches miscdialogsN&Geschwister der ausgewhlten TelibumeSelection's &siblings miscdialogsD&Kinder der ausgewhlten TeilbumeSelection's childre&n miscdialogsKleine Icons Small Icons miscdialogsSortierrichtungSort Direction miscdialogsSortiermethode Sort Method miscdialogs Konten sortieren Sort Nodes miscdialogs&Rechtschreibprfung Spell Check miscdialogs2&Symbolleisten FunktionenTool&bar Commands miscdialogs&Symbolleisten&gre Toolbar &Size miscdialogs(Anzahl SymbolleistenToolbar Quantity miscdialogsWerkzeugTools miscdialogsWerkzeug Men Tools Menu miscdialogs4Schriftart fr BaumanzeigeTree View Font miscdialogs4TreeLine - Schwerer FehlerTreeLine - Serious Error miscdialogs*TreeLine NummerierungTreeLine Numbering miscdialogs4Passwort fr {0} eingeben:Type Password for "{0}": miscdialogs$Passwort eingeben:Type Password: miscdialogs*Nummerierung erneuernUpdate Node Numbering miscdialogs>Datei&verschlsselung verwendenUse file &encryption miscdialogsAnsichtView miscdialogsAnsicht Men View Menu miscdialogs*Nach was wird gesuchtWhat to Search miscdialogs"Was wird sortiert What to Sort miscdialogs0Was soll erneuert werdenWhat to Update miscdialogsFensterWindow miscdialogsFenster Men Window Menu miscdialogsFLeere Passwrter sind nicht erlaubt'Zero-length passwords are not permitted miscdialogs[Alle Felder] [All Fields] miscdialogs[Alle Typen] [All Types] miscdialogsNameName nodeformat^Aktiviere den Daten Editor bei schwebender Maus$Activate data editors on mouse hoveroptiondefaultsAussehen Appearanceoptiondefaults.Automatisches Speichern Auto SaveoptiondefaultsZDie zuletzt benutzte Datei automatisch ffnen!Automatically open last file usedoptiondefaults^Einrckung fr Kinder (In Schriftart Einheiten)+Child indent offset (in font height units) optiondefaults>Klick auf Knoten zum UmbenennenClick node to renameoptiondefaults<Formate im BearbeitungsfensterData Editor Formatsoptiondefaults"DatumsdarstellungDatesoptiondefaults*Verfgbare FunktionenFeatures Availableoptiondefaults Erster WochentagFirst day of weekoptiondefaultsFreitagFridayoptiondefaultshDruckoptimierte TreeLine JSON Dateien mit Einrckung)Indent (pretty print) TreeLine JSON filesoptiondefaultsLMinimiere Programm in die Systemleiste#Minimize application to system trayoptiondefaultsjMinuten zwischen Speichervorgngen (0 fr Abschalten)+Minutes between saves (set to 0 to disable)optiondefaults MontagMondayoptiondefaultsBAnzahl zuletzt geffneter Dateien(Number of recent files in the file menuoptiondefaultsPAnzahl der mglichen WiederherstellungenNumber of undo levelsoptiondefaults<ffne Dateien in neuem FensterOpen files in new windowsoptiondefaults2Zuletzt geffnete Dateien Recent FilesoptiondefaultslLsche alle nicht vorhandenen aktuellen Datei-Eintrge'Remove inaccessible recent file entriesoptiondefaultsNNeue Knoten nach dem Anlegen umbenennenRename new nodes when createdoptiondefaultsDFenstereinteilung wiederherstellen Restore previous window geometryoptiondefaultstBaumansicht von krzlich geffneten Dateien wierherstellen(Restore tree view states of recent filesoptiondefaultsSamstagSaturdayoptiondefaultsDZeige Breadcrumb Vorfahren AnsichtShow breadcrumb ancestor viewoptiondefaultsXZeige Kind Ausschnitt in der rechten Ansicht#Show child pane in right hand viewsoptiondefaultsFUnterknoten in der Ansicht anzeigenShow descendants in output viewoptiondefaultsBIcons in der Baumansicht anzeigenShow icons in the tree viewoptiondefaultsfMathematische Felder im Bearbeiten Fenster anzeigen&Show math fields in the Data Edit viewoptiondefaultsdNummerierungsfelder im Bearbeiten Fenster anzeigen+Show numbering fields in the Data Edit viewoptiondefaultsProgrammstartStartup ConditionoptiondefaultsSonntagSundayoptiondefaultsDonnerstagThursdayoptiondefaultsZeitdarstellungTimesoptiondefaultsXVerschieben von Bumen mit der Maus erlaubenTree drag && drop availableoptiondefaultsDienstagTuesdayoptiondefaults@Speicher fr Wiederherstellungen Undo MemoryoptiondefaultsMittwoch Wednesdayoptiondefaults&Abbrechen&Canceloptions&OK&OKoptions2Ablage Konfigurationsfile"Choose configuration file locationoptionsVProgrammverzeichnis (fr portablen Einsatz)$Program directory (for portable use)options>Benutzerverzeichnis (empfohlen)#User's home directory (recommended)optionsNFehler beim Initialisieren des DruckersError initializing printer printdata4TreeLine - PDF ExportierenTreeLine - Export PDF printdataWarnung: Randeinstellung werden vom derzeitigen Drucker nicht untersttzt. Sollen die Einstellungen berichtigt werden?JWarning: Margin settings unsupported on current printer. Save adjustments? printdata Warnung: Seitengre und Randeinstellung werden vom derzeitigen Drucker nicht untersttzt. Sollen die Einstellungen berichtigt werden?]Warning: Page size and margin settings unsupported on current printer. Save page adjustments? printdataWarnung: Seitengre werden vom derzeitigen Drucker nicht untersttzt. Sollen die Einstellungen berichtigt werden?KWarning: Page size setting unsupported on current printer. Save adjustment? printdata&Unten:&Bottom: printdialogs&Abbrechen&Cancel printdialogs<Linien zu den Kindern zeichnen&Draw lines to children printdialogsGesamter Baum &Entire tree printdialogs&Schriftart&Font printdialogsSchrift&auswahl&Font Selection printdialogs2Allgemeine &Einstellungen&General Options printdialogs Kopfzeile &Links &Header Left printdialogs&Kopf-/Fuzeile&Header/Footer printdialogs2Wurzelelement hinzunehmen&Include root node printdialogs|Das erste Unterelement mit dem Elternelement zusammen anzeigen&Keep first child with parent printdialogs&Links:&Left: printdialogsSpaltenanzahl&Number of columns printdialogs&OK&OK printdialogsPrfi&x&Prefix printdialogs&Drucken... &Print... printdialogs&Rechts:&Right: printdialogs&Suffix&Suffix printdialogs &Oben:&Top: printdialogs&Einheiten&Units printdialogsL&TreeLine Ausgabe Schriftart verwenden&Use TreeLine output view font printdialogs&Breite:&Width: printdialogs"A3 (279 420 mm)A3 (279 x 420 mm) printdialogs"A4 (210 297 mm)A4 (210 x 297 mm) printdialogs"A5 (148 210 mm)A5 (148 x 210 mm) printdialogs>AaBbCcDcEeFfGg...TtUuVvWwXxYyZzAaBbCcDdEeFfGg...TtUuVvWvXxYyZz printdialogsZentimeter (cm)Centimeters (cm) printdialogsSpaltenColumns printdialogs0Benutzerdefinierte Gre Custom Size printdialogs&standard Schriftart Default Font printdialogsfFehler: Seitengre oder Seitenrnder sind ungltig(Error: Page size or margins are invalid printdialogsExtratext Extra Text printdialogs0Gegenberliegende Seiten Facing Pages printdialogsEigenschaftenFeatures printdialogs FelderFiel&ds printdialogsFeldf&ormat Field For&mat printdialogs(Feldformat fr "{0}"Field Format for "{0}" printdialogsSeite anpassenFit Page printdialogsBreite anpassen Fit Width printdialogsSchrifts&til Font st&yle printdialogs&Fuzeile:Foot&er: printdialogs&Fuzeile Links Footer &Left printdialogsFuzeile M&itteFooter Ce&nter printdialogs"Fusszeile &Rechts Footer Righ&t printdialogs,&Hilfe zu den Formaten Format &Help printdialogs&Kopfzeile:He&ader: printdialogs"Kopfzeile &Rechts Header &Right printdialogs Kopfzeile Mitt&eHeader C&enter printdialogs$Kopf- und FuzeileHeader and Footer printdialogs &Hhe:Height: printdialogsInch (in) Inches (in) printdialogs,Bercksichtigte KnotenIncluded Nodes printdialogsEinrckenIndent printdialogsVAbstand beim Einrcken (Einheit Zeilenhhe)"Indent Offse&t (line height units) printdialogs&Querformat Lan&dscape printdialogs*Legal (8.5 14 Inch)Legal (8.5 x 14 in.) printdialogs,Letter (8.5 11 Inch)Letter (8.5 x 11 in.) printdialogs RnderMargins printdialogsMillimeter (mm)Millimeters (mm) printdialogsNchste Seite Next Page printdialogs@Nur Kinder von geffneten KnotenOnl&y open node children printdialogsOrientierung Orientation printdialogsAusgabeformatOutput &Format printdialogs&Seiteneinstellungen Page &Setup printdialogsPapiergre Paper &Size printdialogs&Hochformat Portra&it printdialogsVorherige Seite Previous Page printdialogsDruckenPrint printdialogs"Druck&vorschau...Print Pre&view... printdialogsDruckvorschau Print Preview printdialogs$Druckeinstellungen Print Setup printdialogs$DruckeinstellungenPrinting Setup printdialogsBeispielSample printdialogsWhle &DruckerSelect &Printer printdialogs(Schriftart auswhlen Select Font printdialogs*Ausgewhlte TeilbumeSelected &branches printdialogs$Ausgewhlte KnotenSelected &nodes printdialogs &GreSi&ze printdialogsEinzelne Seite Single Page printdialogs2Abstand zwischen &SpaltenSpace between colu&mns printdialogs,Tabloid (11 17 Inch)Tabloid (11 x 17 in.) printdialogs(TreeLine PDF DruckerTreeLine PDF Printer printdialogs"Was wird gedruckt What to print printdialogsVergrernZoom In printdialogsVerkleinernZoom Out printdialogs&Hinzufgen&Add spellcheck&Abbrechen&Cancel spellcheck"&Alles Ignorieren &Ignore All spellcheck&Ersetzen&Replace spellcheck<Hinzufgen in &KleinbuchstabenAdd &Lowercase spellcheckKontext:Context: spellcheckKann weder aspell.exe, ispell.exe oder hunspell.exe finden. Speicherort suchen?QCould not find either aspell.exe, ispell.exe or hunspell.exe Browse for location? spellcheckRechtschreibprfung fr den Zweig beendet. Wieder von oben anfangen? 3Finished checking the branch Continue from the top? spellcheck6Rechtschreibprfung beendetFinished spell checking spellcheck&IgnorierenIgnor&e spellcheck\Suche aspell.exe, ispell.exe oder hunspell.exe-Locate aspell.exe, ipsell.exe or hunspell.exe spellcheck&Nicht im WrterbuchNot in Dictionary spellcheck Programm (*.exe)Program (*.exe) spellcheckAlles E&rsetzen Re&place All spellcheck&Rechtschreibprfung Spell Check spellcheckBFehler in der RechtschreibprfungSpell Check Error spellcheckVorschlge Suggestions spellcheck8TreeLine RechtschreibprfungTreeLine Spell Check spellcheckFehler bei der Rechtschreibprfung. Stellen Sie sicher, dass aspell, ispell oder hunspell installiert istLTreeLine Spell Check Error Make sure aspell, ispell or hunspell is installed spellcheck Wort:Word: spellcheckAuswahl im BaumSelect in Tree titlelistviewSTANDARDDEFAULT treeformatsFELDFIELD treeformats DATEIFILE treeformatsFELDTYP FieldType treeformats FormatFormat treeformatsIconIcon treeformatsTYPTYPE treeformats&Fettschrift &Bold Fonttreelocalcontrol&Kopieren&CopytreelocalcontrolKnoten &lschen &Delete NodetreelocalcontrolTrenne Klone&Detach Clonestreelocalcontrol&Exportieren... &Export...treelocalcontrol(&Externer Verweis...&External Link...treelocalcontrol"&Schriftart Gre &Font Sizetreelocalcontrol"Knoten &einrcken &Indent Nodetreelocalcontrol&Kursivschrift &Italic Fonttreelocalcontrol,Nach &oben verschieben&Move Uptreelocalcontrol&Neues Fenster &New WindowtreelocalcontrolEin&fgen&Pastetreelocalcontrol&Drucken... &Print...treelocalcontrol"W&iederherstellen&Redotreelocalcontrol*Erstelle Verweise neu&Regenerate Referencestreelocalcontrol&Umbenennen&RenametreelocalcontrolS&peichern&Savetreelocalcontrol&Knoten&typ zuweisen&Set Node Typetreelocalcontrol.&Rechtschreibprfung...&Spell Check...treelocalcontrol&Rckgngig&Undotreelocalcontrol"Knoten &ausrcken&Unindent Nodetreelocalcontrol &Kind hinzufgen Add &ChildtreelocalcontrolLKategorie &Hierarchieebene einfgen...Add Category &Level...treelocalcontrolRNeues Kind zu aktuellem Knoten hinzufgen Add new child to selected parenttreelocalcontrolZEinen externen Verweis hinzufgen oder ndern!Add or modify an extrnal web linktreelocalcontrolNInternen Verweis ndern oder hinzufgen#Add or modify an internal node linktreelocalcontrolxOhne gemeinsame Felder kann der Baum nicht expandiert werden#Cannot expand without common fieldstreelocalcontrolKategoriefelderCategory Fieldstreelocalcontrol.&Formatierung entfernenClear For&mattingtreelocalcontrol|Die Formatierung am aktuellen oder selektierten Text entfernen)Clear current or selected text formattingtreelocalcontrol8Klone alle &passenden KnotenClone All &Matched NodestreelocalcontrolRKinder zusammenfassen und Felder vereinen&Collapse descendants by merging fieldstreelocalcontrolTKonvertiere alle passenden Knoten in Klone&Convert all matching nodes into clonestreelocalcontrol>{0} Zweige in Klone konvertiert"Converted {0} branches into clonestreelocalcontrol8Typen aus &Datei kopieren...Copy Types from &File...treelocalcontrolrDen Teilbaum oder den Text in die Zwischenablage kopieren(Copy the branch or text to the clipboardtreelocalcontrolrKonfiguration aus einer anderen TreeLine-Datei bernehmen1Copy the configuration from another TreeLine filetreelocalcontrol&AusschneidenCu&ttreelocalcontrolDen Teilbaum oder Text ausschneiden und in die Zwischenablage legen'Cut the branch or text to the clipboardtreelocalcontrolStandardDefaulttreelocalcontrol>Die ausgewhlten Knoten lschenDelete the selected nodestreelocalcontrolVTrenne alle Klone Knoten im aktuellen Zweig+Detach all cloned nodes in current branchestreelocalcontrol^Fehler: Kann Sicherheitskopie {0} nicht lschen'Error - could not delete backup file {}treelocalcontrolJFehler - Konnte Datei {0} nicht lesenError - could not read file {0}treelocalcontrolRFehler - konnte nicht auf Datei schreibenError - could not write to filetreelocalcontrolPFehler - konnte nicht nach {0} schreibenError - could not write to {}treelocalcontrolfDie Datei in unterschiedlichen Formaten exportieren(Export the file in various other formatstreelocalcontrol`Nach PDF mit aktuellen Druckoptionen exportieren+Export to PDF with current printing optionstreelocalcontrol"Datei gespeichert File savedtreelocalcontrol2&Abflachen nach KategorieFlatten &by Categorytreelocalcontrol(Schriftart &Farbe...Font C&olor...treelocalcontrolErzwinge Aktualisierung alle konditonalen Bedingungen & Mathematischen Feldern3Force update of all conditional types & math fieldstreelocalcontrolBDie ausgewhlten Knoten einrckenIndent the selected nodestreelocalcontrolF&Nach ausgewhltem Element einfgenInsert Sibling &AftertreelocalcontrolD&Vor ausgewhltem Element einfgenInsert Sibling &Beforetreelocalcontrol`Kategorieebene oberhalb der Unterknoten einfgen$Insert category nodes above childrentreelocalcontrolpEin neues Element nach dem ausgewhlten Element einfgen"Insert new sibling after selectiontreelocalcontrolnEin neues Element vor dem ausgewhlten Element einfgen#Insert new sibling before selectiontreelocalcontrol(&Interner Verweis...Internal &Link...treelocalcontrolGroLargetreelocalcontrol GrerLargertreelocalcontrolAm GrtenLargesttreelocalcontrol.Nach &unten verschieben M&ove Downtreelocalcontrol,Zum Anfang verschieben Move &Firsttreelocalcontrol(Zum Ende verschieben Move &Lasttreelocalcontrol\Die ausgewhlten Knoten nach unten verschiebenMove the selected nodes downtreelocalcontroljDas ausgewhlte Knoten als erstes Unterelement setzen0Move the selected nodes to be the first childrentreelocalcontrollDas ausgewhlte Knoten als letztes Unterelement setzen/Move the selected nodes to be the last childrentreelocalcontrolZDie ausgewhlten Knoten nach oben verschiebenMove the selected nodes uptreelocalcontrolBKeine identischen Knoten gefundenNo identical nodes foundtreelocalcontrolTNeues Fenster fr die gleiche Datei ffnen#Open a new window for the same filetreelocalcontrol,&Drucken einrichten...P&rint Setup...treelocalcontrol$Nu&r Text einfgenPa&ste Plain Texttreelocalcontrol(Kind Knoten einfgen Paste C&hildtreelocalcontrol<Fge geklonten Kind Knoten einPaste Cl&oned ChildtreelocalcontrolZFge geklo&nten Geschwwister Knoten davor einPaste Clo&ned Sibling BeforetreelocalcontrolZFge geklonten Geschwister Knoten &danach einPaste Clone&d Sibling AftertreelocalcontrolBFge Geschwister Knoten d&ahinterPaste Sibling &Aftertreelocalcontrol<Fge Geschwister Knoten &davorPaste Sibling &BeforetreelocalcontroljFge geklonten Kind Knoten von der Zwischenablage ein&Paste a child clone from the clipboardtreelocalcontrolVKind Knoten von der Zwischenablage einfgen%Paste a child node from the clipboardtreelocalcontrolTFge Geschwister Knoten hinter Auswahl einPaste a sibling after selectiontreelocalcontrolNFge Geschwister Knoten vor Auswahl ein Paste a sibling before selectiontreelocalcontrolbFge Geschwister Knoten Klon nach der Auswahl ein%Paste a sibling clone after selectiontreelocalcontrol`Fge Geschwister Knoten Klon vor der Auswahl ein&Paste a sibling clone before selectiontreelocalcontrol`Knoten oder Text von der Zwischenablage einfgen&Paste nodes or text from the clipboardtreelocalcontrolfUnformatierten Text von der Zwischenablage einfgen+Paste non-formatted text from the clipboardtreelocalcontrol(Nach &PDF drucken...Print &to PDF...treelocalcontrol"Druck&vorschau...Print Pre&view...treelocalcontroldBaum basierend auf aktuellen Einstellungen drucken*Print tree output based on current optionstreelocalcontrol"&Eigenschaften...Prop&erties...treelocalcontrolDDie letzte Aktion wiederherstellenRedo the previous undotreelocalcontrolLDen Titel des aktuellen Knotens ndern#Rename the current tree entry titletreelocalcontrol.Tausche Kategorie EbeneS&wap Category Levelstreelocalcontrol&Speichern &unter... Save &As...treelocalcontrolDatei speichern Save Filetreelocalcontrol<nderungen an {0} abspeichern?Save changes to {}?treelocalcontrol.nderungen abspeichern? Save changes?treelocalcontrol0Aktuelle Datei speichernSave the current filetreelocalcontrolVDie Datei unter einem neuen Namen speichernSave the file with a new nametreelocalcontrolFFelder fr die neue Ebene auswhlenSelect fields for new leveltreelocalcontrol.Schriftart Gre setzen Set Font Sizetreelocalcontrol$Knotentyp zuweisen Set Node TypetreelocalcontrolDatei Eigenschaften wie Komprimierung oder Verschlsselung setzen3Set file parameters like compression and encryptiontreelocalcontrolbRnder, Seitengre und andere Druckeinstellungen1Set margins, page size and other printing optionstreelocalcontrolvDie Gre der aktuellen oder selektierten Schriftart setzen(Set size of the current or selected texttreelocalcontrolvDie Farbe der aktuellen oder ausgewhlten Schriftart setzen-Set the color of the current or selected texttreelocalcontroltDie aktuelle oder ausgewhlte Schriftart auf "fett" setzen(Set the current or selected font to boldtreelocalcontrolxDie aktuelle oder ausgewhlte Schriftart auf "kursiv" setzen*Set the current or selected font to italictreelocalcontrolDie aktuelle oder ausgewhlte Schriftart auf "unterstreichen" setzen-Set the current or selected font to underlinetreelocalcontrolTDen Typ fr den ausgewhlten Knoten setzen$Set the node type for selected nodestreelocalcontrolJVorschau der Druckergebnisse anzeigen"Show a preview of printing resultstreelocalcontrol KleinSmalltreelocalcontrol8Rechtschreibprfung fr Text Spell check the tree's text datatreelocalcontrolNTausche Kind und Enkel Kategorie Knoten(Swap child and grandchild category nodestreelocalcontrolPTreeLine - Konfigurationsdatei schreiben"TreeLine - Open Configuration Filetreelocalcontrol4TreeLine - Speichern unterTreeLine - Save Astreelocalcontrol&UnterstreichenU&nderline FonttreelocalcontrolFDie letzte Aktion rckgngig machenUndo the previous actiontreelocalcontrolDie ausgewhlten Knoten "ausrcken" (um eine Ebene nach links verschieben) Unindent the selected nodestreelocalcontrolWarnung - Datei Fehler! berspringe fehlerhafte Kind Verweise in den folgenden Knoten:OWarning - file corruption! Skipped bad child references in the following nodes:treelocalcontrol"ber &TreeLine...&About TreeLine...treemaincontrol6&Grundlegende Verwendung...&Basic Usage...treemaincontrol.Datei ffnen &abbrechen&Cancel File Opentreemaincontrol(&Bedingtes Suchen...&Conditional Find...treemaincontrol8&Datentypen konfigurieren...&Configure Data Types...treemaincontrol0Sicherungskopie &lschen&Delete Backuptreemaincontrol&Suche Text... &Find Text...treemaincontrol8&Umfassende Dokumentation...&Full Documentation...treemaincontrol8Allgemeine &Einstellungen...&General Options...treemaincontrol&Importieren... &Import...treemaincontrol&Neu...&New...treemaincontrol&ffnen...&Open...treemaincontrol&Beenden&QuittreemaincontrolBSicherungskopie &wiederherstellen&Restore Backuptreemaincontrol &Alles auswhlen &Select Alltreemaincontrol&&Beispiel auswhlen&Select Sampletreemaincontrol &Vorlagenauswahl&Select Templatetreemaincontrol&Textfilter...&Text Filter...treemaincontrolSicherheitskopie {} existiert. Eine vorherige Sitzung ist mglicherweise abgestrztEine nicht TreeLine Datei ladenOpen a non-TreeLine filetreemaincontrol2Eine Beispieldatei ffnenOpen a sample filetreemaincontrol.Text in Knoten ersetzen!Replace text strings in node datatreemaincontrolBGesamten Text im Editor auswhlenSelect all text in an editortreemaincontrol@&Tastaturkrzel konfigurieren...Set &Keyboard Shortcuts...treemaincontrolLBenutzereinstellungen fr alle Dateien"Set user preferences for all filestreemaincontrol@Zeige Struktur der Konfiguration Show C&onfiguration Structure...treemaincontrolbZeige nur-lesende Visualisierung der Typ Struktur.Show read-only visualization of type structuretreemaincontrol(Knoten&sortierung...Sor&t Nodes...treemaincontrol,Eine neue Datei ffnenStart a new filetreemaincontrol.TreeLine - Datei ffnenTreeLine - Open Filetreemaincontrol@TreeLine grundlegende VerwendungTreeLine Basic Usagetreemaincontrol(TreeLine Version {0}TreeLine version {0}treemaincontrol2&Nummerierung anpassen...Update &Numbering...treemaincontrol2Die Nummerierung anpassenUpdate node numbering fieldstreemaincontrolDFeldbasierte Bedingungen verwenden$Use field conditions to filter nodestreemaincontrollFeldbasierte Bedingungen verwenden um Knoten zu finden"Use field conditions to find nodestreemaincontrolVWarnung - Socket kann nicht geffnet werden'Warning: Could not create local sockettreemaincontrol*fehlendes Verzeichnismissing directorytreemaincontrol&geschrieben von {0}written by {0}treemaincontrolNeuNewtreenodeHauptprogrammMain treestructurePBedingtes Filter hat {0} Knoten gefunden&Conditional filtering, found {0} nodestreeviewJFiltern nach {0}, {1} Knoten gefunden#Filtering by "{0}", found {1} nodestreeviewNchste: {0} Next: {0}treeview:Nchste: {0} (nicht gefunden)Next: {0} (not found)treeviewSuche nach: Search for:treeviewSuche nach: {0}Search for: {0}treeview@Suche nach: {0} (nicht gefunden)Search for: {0} (not found)treeview$Fenster &schlieen &Close Window treewindow@Teilbaum vollstndig &einklappen&Collapse Full Branch treewindow Da&ten&Data treewindow&Bearbeiten&Edit treewindowBTeilbaum vollstndig &expandieren&Expand Full Branch treewindow &Datei&File treewindow &Hilfe&Help treewindow*&Nachfolgende Auswahl&Next Selection treewindow&Knoten&Node treewindow$&Vorherige Auswahl&Previous Selection treewindow(&Kinder mit anzeigen&Show Child Pane treewindow&Extras&Tools treewindow&Ansicht&View treewindow&Fenster&Window treewindow0Dieses Fenster schlieenClose this window treewindow\Alle Kinder der selektierten Knoten einklappen+Collapse all children of the selected nodes treewindow Datenbearbeitung Data Edit treewindowDatenansicht Data Output treewindow^Alle Kinder der selektierten Knoten expandieren)Expand all children of the selected nodes treewindowFo&rmatFo&rmat treewindowPDie folgende Selektion wieder aktivieren(Go to the next tree selection in history treewindow6Nchste inkrementelle SucheNext Incremental Search treewindow:Vorherige inkrementelle SuchePrevious Incremental Search treewindowPZu der vorherigen Selektion zurckkehren%Return to the previous tree selection treewindow2Zeige &Breadcrumb AnsichtShow &Breadcrumb View treewindow6&berschriftsliste anzeigenShow &Title List treewindow Edit&or anzeigenShow Data &Editor treewindow"&Ansicht anzeigenShow Data &Output treewindowDKinder in der Ausgabe mit anzeigenShow Output &Descendants treewindow`Den Editor in der rechten Fensterhlfte anzeigenShow data editor in right view treewindownDie Daten-Ansicht in der rechten Fensterhlfte anzeigenShow data output in right view treewindowzDie berschriften-Liste in der rechten Fensterhlfte anzeigenShow title list in right view treewindow4Starte inkrementelle SucheStart Incremental Search treewindow"berschriftsliste Title List treewindowTUmschalten zu Breadcrumb Vorfahren Ansicht'Toggle showing breadcrumb ancestor view treewindowfDie Kunde in der Ausgabeansicht eingerckt anzeigen/Toggle showing output view indented descendants treewindowZDie Kinder in der Ausgabeansicht mit anzeigen%Toggle showing right-hand child views treewindowTreeLine/translations/treeline_es.qm0000644000175000017500000030514013715363644016640 0ustar dougdougzg1DhDm6 x<|e  !eO~%*D01^^0KU7[8nH5 H5 UMVE c s"ExR5y#Ee/%0KH|rr27~;&p**y*^*^*%_*Tچ*J*0_*5+5t+U`:++=f+++MV+:ު+į+aP/5&+5S4P;0&}m$$ fE333MfEBrJTGUM ϋynЄjBsX1d6p7(S1Ah:l9:7a8QAQ Ye#/01P 4FT4M>52{[B"PE%H*aLW*Lef`/`j.v?ot |{y~sB~B6mwbVK/^hKb. ?M M׽Puܷ"݊  N9Q/cD ! h1 e'U _(OY)yN-d9/c52t oOPՇUicf}4Jlemyn6rjs6 uwU\y\}X=+4IeA2HdSa=0V'w9w96*6Dv7gc"zLr 3 f3aP[y&wiMC=$(nٻﶰ.~)}8sFey*y)3|cm /%v_-'@6n6n,6nF;Y \=}>6nIGsLR5._ c g@lx?|§#³:^GI֓֓Do^.ںTAU` x5y!UT .W_<TZ&=b(+$5n6a>>?d @B@{hoFJ%\8q`}@f4gNehTXhѾrc3};N`Rmc]V96G% 5G?c^N J>?lTY(N.$k+%O"D1ϑĮeT+לI_שD/ؙT::KgZkH^ba&SU!m{mNstd0[FѤcyD~n9#UZ#COC#C#&$*,;.C///0?In38L RSS".ZzZG]_Pb#e,fOfȊgl=(r^*Phm^nk ZQITi*1r~|wϥ4Y̿..R e@˃Ψ'H7ie47pI_LE>t[d(s4%YK.&k o&tE(m3K)k/0nd7Ob`8OaCdP[ "R9NSU4Uly]1[hak_kN<nY*nnpjS{}\'n#oRXFN:u/DB" [F*Ǩx˛qO}eQ"u E3X^JfO{ų\M* ='a_"ԟ*Se,0=1cD1cDi9HH@ETFyLC~W1Y|[%{]!bbn#mJ #9Z>cicac "BHnjTz_֤©kM%*)we9=o4.^ #`%%V%%x[ṙ2.<%UWԤ?v޹d߂ߗevv+^Xm ҷ  n \~ \~ ;9K q#- !; 4E 5 :rK F LD! Mgl f+>c kJ 8D  % ωb \eJ b T b* ld x ߡz H U ]  ʵ1 th nb P Z  j ]3 cY} v & . t T  W75 ,_$1 /5F 4b 5r 7*< = IT` Ty UI Wܾ8 _҉)I a cI) c e 8 uo u5 w5 ă_' 5E 1 3~ : I I I IB I Iv Iف I? Cr 8r: 9d` (  ,^ 6"AZ 5V ' 6T ^ I > 5 t ˈ* ĽN:; ;: Gc$H : : JB ; ;Q 5z e3 & > s . D t X KE )M! -8 .2,# /. 2  55r 93) @(e Al KsGs K( L.< L98 W% X [tf pI3} rW x( c x( {u[; :3.A % s  H  ! > Z: i[S >$ DL (t ]o+ M g D) fq B ~7+ ш t ٷ ߺ KGa| ~9 \E% iG ^$M y~c : v  [ EN^ E= +Cu P % #= Q lg d '{ (EG * 2l$ 7"- 8Ǒq ;y: =! @|5 @6 V>Y dMi g#3 n/ pkx t*# t&J tu v" a {B =L1 D eN hO M^ TC R ^ , C- 4 em m)v үW v bnb DA U( / Yi L \< -| F c ' .[ 0uOz 0 7y9 CG G9.i H3m Kz9 KI^ M Nh ]`^g} _ t dT i5 i$ i$V zG J 2  ݃ |C =2f c*j ˜SR D T# sO ~t >B s :fz eIc"T`6ssV(t'({Q-K/+i0/aS0>a9O= %BEj;H;}L`g(L L|L^_V8 YiZrhji?kjlfcCp#Gqtt/C}~DBw^A4B-rtZfOb xt2:k~?-`Eoԕ Ձ]iYe09hy tipxVC#y`USw;>`h Kc wR(J68 qwE<@-*HNICMUZ8n?[Ke3em3PiFW@rs$ Xww|*\}$7^?y,A 2ӝZԲ9 o]#tg2IkmuHn#HkFs*o4u+)$&|ɟ)u~!]>Yxo;+o_dLp36fwil&Cancelar&Cancelcolorset&OK&OKcolorsetColor de Tema Color Themecolorset&Aadir &Nueva Regla &Add New Rule conditional&Cancelar&Cancel conditional&Cerrar&Close conditional&Borrar&Delete conditional &Terminar filtro &End Filter conditional&Filtro&Filter conditionalCar&gar&Load conditional&OK&OK conditional&Eliminar Regla &Remove Rule conditionalGuar&dar&Save conditional FalsoFalse conditional"Buscar &Siguiente Find &Next conditional B&uscar AnteriorFind &Previous conditionalNombre:Name: conditionalTCoincidencias condicionales no encontradas!No conditional matches were found conditionalTipo de nodo Node Type conditionalRegla {0}Rule {0} conditional Reglas Guardadas Saved Rules conditionalVerdaderoTrue conditional"[Todos los tipos] [All Types] conditionalyand conditionalcontienecontains conditionaltermina con ends with conditionaloor conditionalcomienza con starts with conditional&Aplicar&Apply configdialog&Cancelar&Cancel configdialogTipo de &Datos &Data Type configdialog&Eliminar tipo &Delete Type configdialog,&Derivado del original&Derive from original configdialog&Ecuacin &Equation configdialog0Configuracin de &Campos &Field Config configdialogTip&o de Campo &Field Type configdialog"&Ocultar Avanzado&Hide Advanced configdialogMover Aba&jo &Move Down configdialog&Nuevo Campo... &New Field... configdialog&Nuevo Tipo... &New Type... configdialog&OK&OK configdialogP&refijo&Prefix configdialog&Reiniciar&Reset configdialog&Resultado Tipo &Result Type configdialog"&Seleccionar Todo &Select All configdialog"&Mostrar Avanzado&Show Advanced configdialog.C&riterio de Ordenacin&Sort Criteria configdialog$&Formato de Ttulo &Title Format configdialogHAgregar lnea en &blanco entre nodosAdd &blank lines between nodes configdialogAadir Campo Add Field configdialogAadir tipoAdd Type configdialog<Aadir o Quitar Datos de TiposAdd or Remove Data Types configdialog2Agregar &vietas al textoAdd text bullet&s configdialog^Permitir &HTML con formato de texto enriquecidoAllow &HTML rich text in format configdialog,Operadores AritmticosArithmetic Operators configdialog"Tipos AutomticosAutomatic Types configdialog6Lista de &Campos DisponibleAvailable &Field List configdialog&Campos &DisponiblesAvailable &Fields configdialog$Resultado BooleanoBoolean Result configdialogNo se puede eliminar un tipo de datos que est siendo usado por algn nodo+Cannot delete data type being used by nodes configdialogCambiar &Icono Change &Icon configdialog"Recuento de Hijos Child Count configdialogReferencia HijaChild Reference configdialog2Lmites de los Tipos HijoChild Type Limits configdialog"Borrar &Seleccin Clear &Select configdialogCo&piar Tipo... Co&py Type... configdialogdCombinacin && &separador lista de hijos de Salida+Combination && Child List Output &Separator configdialog.Operadores ComparativosComparison Operators configdialog2Configurar Tipos de DatosConfigure Data Types configdialogCopiar Tipo Copy Type configdialog4Crear Tipos Co&ndicionalesCreate Co&nditional Types configdialog$Resultado de Fecha Date Result configdialogH&Valor por Defecto para Nuevos NodosDefault &Value for New Nodes configdialog,Tipo Hi&jo por DefectoDefault Child &Type configdialog Definir EcuacinDefine Equation configdialogNDefinir el Campo de Ecuacin MatemticaDefine Math Field Equation configdialog&Borrar Campo Dele&te Field configdialog6Derivado del Tipo &GenricoDerived from &Generic Type configdialogDescripcin Description configdialogDireccin Direction configdialog"Altura del editor Editor Height configdialogJIntroduzca el nombre del nuevo campo:Enter new field name: configdialogBEstablecer nombre del nuevo tipo:Enter new type name: configdialog*Erron en ecuacin: {}Equation error: {} configdialogzError - referencia circular en ecuaciones de campo matemtico2Error - circular reference in math field equations configdialog.Evaluar etiquetas &HTMLEvaluate &HTML tags configdialogTexto Adicional Extra Text configdialog Cam&poF&ield configdialog &Lista de Campos F&ield List configdialog CampoField configdialog Lista de &Campos Field &List configdialog(Referencias de CampoField References configdialogHReferencia de informacin de archivoFile Info Reference configdialog$Cam&biar DireccinFlip &Direction configdialog(Ayuda sobre &Formato Format &Help configdialog IconoIcon configdialog&Ecuacin matemtica Math Equation configdialog4Modificar &Lista de CamposModify &Field List configdialog<Modificar Tipos Co&ndicionalesModify Co&nditional Types configdialogMover A&rribaMove &Up configdialogMover Aba&jo Move Do&wn configdialogMo&ver ArribaMove U&p configdialog NombreName configdialogNadaNone configdialog4Nmero de lneas de te&xtoNum&ber of text lines configdialog$Resultado Numrico Number Result configdialog"Tipo de O&peradorO&perator Type configdialog &Salida de DatosO&utput configdialog(Lista de Oper&adoresOper&ator List configdialogOperaciones Operations configdialog6Otras Referencias de CamposOther Field References configdialog$&Formato de SalidaOut&put Format configdialog$Formato de Sa&lidaOutpu&t Format configdialogSalida HTML Output HTML configdialog$Opciones de SalidaOutput Options configdialog Referencia PadreParent Reference configdialog&Tipo de Refere&nciaRefere&nce Type configdialog(Referencia de &NivelReference &Level configdialog&Referencia de &TipoReference &Type configdialog(Niv&el de ReferenciaReference Le&vel configdialog&Renombrar Cam&po...Rena&me Field... configdialog$Renom&brar Tipo...Rena&me Type... configdialogRenombrar Campo Rename Field configdialogRenombrar tipo Rename Type configdialog$Renombrar de {} a:Rename from {} to: configdialogReferencia RazRoot Reference configdialog(Seleccionar &Ninguno Select &None configdialogAutoreferenciaSelf Reference configdialogJEstablecer el Icono del Tipo de DatosSet Data Type Icon configdialogBConfigurar Tipos CondicionalmenteSet Types Conditionally configdialog$Ord&enar Claves... Sort &Keys... configdialog,Clave de ClasificacinSort Key configdialog(Ordenar Campos ClaveSort Key Fields configdialogSufi&joSuffi&x configdialogLista de &Tipos T&ype List configdialog&Operadores de TextoText Operators configdialog Texto Resultante Text Result configdialogbLos siguientes caracteres no estn permitidos: {},The following characters are not allowed: {} configdialog<El nombre no puede estar vacoThe name cannot be empty configdialogHEl nombre no puede contener espaciosThe name cannot contain spaces configdialogJEl nombre no puede comenzar con "xml" The name cannot start with "xml" configdialogJEl nombre debe comenzar con una letra!The name must start with a letter configdialog0El nombre ya est en usoThe name was already used configdialog&Resultado de Tiempo Time Result configdialog Configurar &Tipo Typ&e Config configdialogTipoType configdialog@Mostrar datos de campo en tab&laUse a table for field &data configdialog:[Todos los Tipos Disponibles][All Types Available] configdialog [Nada][None] configdialogvalor absolutoabsolute value configdialog sumaradd configdialogarcocoseno arc cosine configdialogarcosenoarc sine configdialogarcotangente arc tangent configdialogpromedioaverage configdialog(logaritmo en base 10base-10 logarithm configdialog concatenar textoconcatenate text configdialog8convertir texto a minsculasconvert text to lower case configdialog8convertir texto a maysculasconvert text to upper case configdialog coseno de radincosine of radians configdialog"grados a radianesdegrees to radians configdialogdividirdivide configdialogigual aequal to configdialogfactorial factorial configdialogpunto flotantefloating point configdialog*divisin por redondeo floor divide configdialogadelanteforward configdialogAdelantefwd configdialogmayor que greater than configdialog"mayor o igual quegreater than or equal to configdialogentero mayorhigher integer configdialogen primer argumento, reemplazar segundo argumento con tercer argumento(in 1st arg, replace 2nd arg with 3rd arg configdialoghunir texto usando como primer argumento un separador$join text using 1st arg as separator configdialogmenor que less than configdialog"menor o igual queless than or equal to configdialoglgico y logical and configdialoglgico o logical or configdialogentero menor lower integer configdialog mximomaximum configdialog mnimominimum configdialog mdulomodulus configdialogmultiplicarmultiply configdialog6constante logaritmo naturalnatural log constant configdialog"logaritmo naturalnatural logarithm configdialogdistinto a not equal to configdialog*nmero pi (constante) pi constant configdialogpotenciapower configdialog"radianes a gradosradians to degrees configdialog Atrsrev configdialog atrsreverse configdialog:redondear a nmero de dgitosround to num digits configdialogseno de radinsine of radians configdialograz cuadrada square root configdialog restarsubtract configdialogsuma de tems sum of items configdialog$Tangente de Radintangent of radians configdialogverdadero si el argumentdo del primer texto contiene un segundo argumento%true if 1st text arg contains 2nd arg configdialogverdadero si el argumentdo del primer texto termina con un segundo argumento&true if 1st text arg ends with 2nd arg configdialogverdadero si el argumentdo del primer texto comienza con un segundo argumento(true if 1st text arg starts with 2nd arg configdialogNvalor verdadero, condicin, valor falso"true value, condition, false value configdialogentero truncadotruncated integer configdialog(&Seleccionar Archivo&Browse for File dataeditors&Cancelar&Cancel dataeditors&Ir al Destino &Go to Target dataeditors&OK&OK dataeditors&Abrir Enlace &Open Link dataeditors&Abrir Imagen &Open Picture dataeditorsJ(Clicar objeto a enlazar en el rbol)(Click link target in tree) dataeditorsAbsolutaAbsolute dataeditorsDireccinAddress dataeditorsBorrar En&lace Clear &Link dataeditorsMostrar Nombre Display Name dataeditorsEnlace Externo External Link dataeditors.Tipo de ruta de archivoFile Path Type dataeditorsEnlace Interno Internal Link dataeditorsAbrir &Carpeta Open &Folder dataeditors Enlace de Imagen Picture Link dataeditorsRelativaRelative dataeditorsEsquemaScheme dataeditors"Establecer A&hora Set to &Now dataeditorsFecha de &Hoy Today's &Date dataeditorsFTreeLine - Enlace a archivo externoTreeLine - External Link File dataeditors8TreeLine - Archivo de ImagenTreeLine - Picture File dataeditors enlacelink dataeditors&Columnas&ColumnsexportsTabla &de descendientes delimitada por comas (CSV) (nmero de niveles);&Comma delimited (CSV) table of descendants (level numbers)exports&Todo el rbol &Entire treeexports &HTML&HTMLexports4Formato &HTML de favoritos&HTML format bookmarksexports6&Incluir noddos principales&Include root nodesexports(&ODF Texto (esquema) &ODF Outlineexports4&Antiguos TreeLine (2.0.x)&Old TreeLine (2.0.x)exports.Abrir s&olo nodos hijos&Only open node childrenexports&Pgina HTML &Simple&Single HTML pageexportsB&Texto con tabulacin por ttulos&Tabbed title textexports &Texto&Textexports$&Subrbol TreeLine&TreeLine SubtreeexportsL&Salida sin formato para todo el texto&Unformatted output of all textexports4Formato &XBEL de favoritos&XBEL format bookmarksexports&XML (genrico)&XML (generic)exports&Favoritos Book&marksexportsFavoritos BookmarksexportsXSeleccionar subtipo de formato para exportarChoose export format subtypeexportsRSeleccionar tipo de formato para exportarChoose export format typeexportsDSeleccionar opciones para exportarChoose export optionsexportspT&abla de hijos delimitada por comas (CSV) (nivel nico)7Comma &delimited (CSV) table of children (single level)exportsError: No se puede vincular al archivo TreeLine no guardado. Guarde el archivo y vuelva a intentarlo.FError - cannot link to unsaved TreeLine file. Save the file and retry.exportsError - Exportar archivos de plantilla no encontrados. Verifique la instalacin de su TreeLine.JError - export template files not found. Check your TreeLine installation.exports Exportar Archivo File ExportexportsLIncluir im&presin de encabezado y pieInclude &print header && footerexportsVista de rbol en tiempo real, vinculada al archivo TreeLine (para servidor web)8Live tree view, linked to TreeLine file (for web server)exportsVista de rbol en tiempo real, archivo nico (datos incrustados)+Live tree view, single file (embedded data)exports>Mltiples tablas de datos &HTMLMultiple HTML &data tablesexports^&Multiples pginas HTML con panel de navegacin)Multiple HTML &pages with navigation paneexportsPDebe seleccionar nodos antes de exportar!Must select nodes prior to exportexports@Nive&les del Panel de NavegacinNavigation pane &levelsexportsOtras Opciones Other Optionsexports PadreParentexports(&Ramas SeleccionadasSelected &branchesexports(&Nodos SeleccionadosSelected &nodesexportsV&Pgina HTML simple con panel de navegacin&Single &HTML page with navigation paneexportsTa&bla delimitada por tabulaciones segn nodos hijos (nivel nico)0Tab &delimited table of children (&single level)exportsTree&Line Tree&Lineexports@TreeLine - Exportar XML GenricoTreeLine - Export Generic XMLexports0TreeLine - Exportar HTMLTreeLine - Export HTMLexportsDTreeLine - Exportar Favoritos HTML TreeLine - Export HTML Bookmarksexports$Exportar Texto ODFTreeLine - Export ODF Textexports>TreeLine - Exportar Texto PlanoTreeLine - Export Plain TextexportsFTreeLine - Exportar Tablas de TextoTreeLine - Export Text TablesexportsJTreeLine - Exportar Textos de TtulosTreeLine - Export Text TitlesexportsJTreeLine - Exportar Subrbol TreeLine"TreeLine - Export TreeLine SubtreeexportsDTreeLine - Exportar Favoritos XBEL TreeLine - Export XBEL BookmarksexportsAdvertencia: no hay una ruta relativa entre "{0}" y "{1}". Continuar con la ruta absoluta?LWarning - no relative path from "{0}" to "{1}". Continue with absolute path?exportsQu exportarWhat to Exportexports"." Carcter .."." Character .. fieldformat"/" Carcter //"/" Character // fieldformat(0 1 Repeticiones ?0 Or 1 Repetitions ? fieldformat,0 o ms repeticiones *0 Or More Repetitions * fieldformat,1 o ms repeticiones +1 Or More Repetitions + fieldformatAM/PM %pAM/PM %p fieldformat(Cualquier Carcter .Any Character . fieldformatAutoEleccin AutoChoice fieldformatAutoCombinacinAutoCombination fieldformatBooleanoBoolean fieldformatMaysculas ACapital Letter A fieldformat<Nmeros Romanos (maysculas) ICapital Roman Numeral I fieldformatEleccinChoice fieldformatCombinacin Combination fieldformat"Separador Coma \,Comma Separator \, fieldformat FechaDate fieldformatFecha y horaDateTime fieldformat.Da (1 2 dgitos) %-dDay (1 or 2 digits) %-d fieldformat$Da (2 dgitos) %dDay (2 digits) %d fieldformat2Da del ao (1 a 366) %-jDay of year (1 to 366) %-j fieldformatComa Decimal ,Decimal Comma , fieldformatPunto Decimal .Decimal Point . fieldformat&Cuenta DescendienteDescendantCount fieldformatDDgito o Espacio (externo) <space>!Digit or Space (external)  fieldformat$Separador Punto \.Dot Separator \. fieldformat"Final del Texto $ End of Text $ fieldformat4Escape carcter especial \Escape a Special Character \ fieldformatEjemplo 1/2/3/4Example 1/2/3/4 fieldformat.Exponente (mayscula) EExponent (capital) E fieldformat.Exponente (minscula) eExponent (small) e fieldformatEnlace Externo ExternalLink fieldformat<Hora (0-23, 1 2 dgitos) %-HHour (0-23, 1 or 2 digits) %-H fieldformat4Hora (00-23, 2 dgitos) %HHour (00-23, 2 digits) %H fieldformat4Hora (01-12, 2 dgitos) %IHour (01-12, 2 digits) %I fieldformat<Hora (1-12, 1 2 dgitos) %-IHour (1-12, 1 or 2 digits) %-I fieldformatTexto HtmlHtmlText fieldformatEnlaceInterno InternalLink fieldformat(Separador de nivel /Level Separator / fieldformat Minsculas [a-z]Lower Case Letters [a-z] fieldformatMatemticasMath fieldformat8Microsegundos (6 dgitos) %fMicroseconds (6 digits) %f fieldformat6Minutos (1 2 dgitos) %-MMinute (1 or 2 digits) %-M fieldformat,Minutos (2 dgitos) %MMinute (2 digits) %M fieldformat.Mes (1 o 2 dgitos) %-mMonth (1 or 2 digits) %-m fieldformat$Mes (2 dgitos) %mMonth (2 digits) %m fieldformat$Mes (abreviado) %bMonth Abbreviation %b fieldformat0Mes (nombre completo) %B Month Name %B fieldformat,No es un nmero [^0-9]Not a Number [^0-9] fieldformat AhoraNow fieldformat NmeroNumber fieldformatNmero 1Number 1 fieldformatNumeracin Numbering fieldformat$Texto de una lnea OneLineText fieldformat"Dgito Opcional #Optional Digit # fieldformat Signo Opcional -Optional Sign - fieldformatO |Or | fieldformatHEjemplo de esquema I../A../1../a)/i)!Outline Example I../A../1../a)/i) fieldformat ImagenPicture fieldformat"Expresin regularRegularExpression fieldformat$Dgito Requerido 0Required Digit 0 fieldformat"Signo Requerido +Required Sign + fieldformat8Segundos (1 2 dgitos) %-SSecond (1 or 2 digits) %-S fieldformat.Segundos (2 dgitos) %SSecond (2 digits) %S fieldformat4Ejemplo de seccin 1.1.1.1Section Example 1.1.1.1 fieldformat,Separador de seccin .Section Separator . fieldformatSeparador / Separator / fieldformat2Conjunto de Nmeros [0-9]Set of Numbers [0-9] fieldformatMinsculas aSmall Letter a fieldformat<Nmeros romanos (minsculas) iSmall Roman Numeral i fieldformatFSeparador Espacio (interno) <space>"Space Separator (internal)  fieldformatTexto espaciado SpacedText fieldformatV/FT/F fieldformat TextoText fieldformatHoraTime fieldformat Maysculas [A-Z]Upper Case Letters [A-Z] fieldformat&Semana (0 a 53) %-UWeek Number (0 to 53) %-U fieldformat@Da de la semana (abreviado) %aWeekday Abbreviation %a fieldformat&Da de la semana %AWeekday Name %A fieldformatS/NY/N fieldformat$Ao (2 dgitos) %yYear (2 digits) %y fieldformat$Ao (4 dgitos) %YYear (4 digits) %Y fieldformatverdadero/falso true/false fieldformat s/noyes/no fieldformat falsofalse genbooleannono genbooleanverdaderotrue genbooleansyes genboolean$Todos los Archivos All Files globalref6Todos los Archivos TreeLineAll TreeLine Files globalrefFArchivos CSV (Delimitado por Comas)CSV (Comma Delimited) Files globalrefArchivos HTML HTML Files globalref*Archivos de Texto ODFODF Text Files globalref4Archivos TreeLine AntiguosOld TreeLine Files globalrefArchivos PDF PDF Files globalref"Archivos de Texto Text Files globalref"Archivos TreeLineTreeLine Files globalref<Archivos TreeLine - ComprimidoTreeLine Files - Compressed globalref<Archivos TreeLine - EncriptadoTreeLine Files - Encrypted globalref&Archivos de Treepad Treepad Files globalrefArchivos XML XML Files globalref Buscar:  Find: helpview&Volver&Backhelpview&Siguiente&Forwardhelpview&Inicio&Homehelpview"&Buscar Siguiente Find &Nexthelpview Buscar &AnteriorFind &Previoushelpview:Cadena de texto no encontradaText string not foundhelpviewHerramientasToolshelpview"{0}" no es un archivo vlido TreeLine. Usar filtro de importacin de archivo?:"{0}" is not a valid TreeLine file. Use an import filter?importsRXML &genrico (No es un archivo TreeLine) &Generic XML (non-TreeLine file)importsB&HTML favoritos (Formato Mozilla) &HTML bookmarks (Mozilla Format)importsl&Sangrado de texto por tabulaciones, un nodo por lnea%&Tab indented text, one node per lineimports@Favoritos en &XML (Formato XBEL)&XML bookmarks (XBEL format)importsFAVORITOBOOKMARKimports@Formato CSV.errneo en Lnea {0}Bad CSV format on Line {0}importsFavoritos Bookmarksimports:Escoger Mtodo de ImportacinChoose Import MethodimportsT&exto delimitado por comas (CSV) tabla de texto con nivel de columna y fila de encabezadoACo&mma delimited (CSV) text table with level column && header rowimports|Texto &delimitado por comas (CSV) tabla con fila de encabezado1Comma delimited (CSV) text table &with header rowimportsDError-no se puede leer archivo {0}Error - could not read file {0}importsBError - formato incorrecto en {0}Error - improper format in {0}importsCARPETAFOLDERimports Importar Archivo Import Fileimports"Archivo no Vlido Invalid FileimportsRNmero de nivel no vlido en la lnea {0} Invalid level number on line {0}imports:Estructura de nivel no vlidaInvalid level structureimports EnlaceLinkimportsPArchivo Antiguo de Tree&Line (1.x o 2.x)Old Tree&Line File (1.x or 2.x)imports(Open &Document (ODF)Open &Document (ODF) outlineimportsOtroOtherimportsrTexto &plano en prrafos (delimitado por lnea en blanco)-Plain text ¶graphs (blank line delimited)importsText&o plano, un &nodo por lnea (delimitado por retorno de carro -CR-)-Plain text, one &node per line (CR delimited)importsSEPARADOR SEPARATORimports TABLATABLEimportsTabl&a de texto delimitada por tabulaciones con fila de encabezado)Tab delimited text table with header &rowimports TextoTextimportsFDemasiadas entradas en la Lnea {0}Too many entries on Line {0}imports6TreeLine - Importar archivoTreeLine - Import FileimportsX&Archivo Treepad (nicamente nodos de texto)Treepad &file (text nodes only)importsvLas referencias secundarias deben combinarse en una funcin/Child references must be combined in a functionmatheval:Caracteres "{}" no permitidosIllegal "{}" charactersmatheval8Funcin ilegal presente: {0}Illegal function present: {0}mathevalVTipo de objeto u operador no permitido: {0}$Illegal object type or operator: {0}matheval<Sintaxis ilegar en la ecuacinIllegal syntax in equationmathevalA&plicar&Apply miscdialogs&Cancelar&Cancel miscdialogs&Cerrar&Close miscdialogsC&errar Filtro &End Filter miscdialogsrbol compl&eto &Entire tree miscdialogs&Filtro&Filter miscdialogs"&Buscar Siguiente &Find Next miscdialogs&Adelante&Forward miscdialogs"&Ignorar y omitir&Ignore and skip miscdialogs&Contiene &Key words miscdialogsTipo de &Nodo &Node Type miscdialogs&OK&OK miscdialogs>Nombres de Campos &Predefinidos&Predefined Key Fields miscdialogs$Exp&resin regular&Regular expression miscdialogs&Reemplazar&Replace miscdialogs^&Reiniciar nmeros para los siguientes hermanos"&Restart numbers for next siblings miscdialogs>&Restaurar Valores por &Defecto&Restore Defaults miscdialogs At&rs&Reverse miscdialogs&Buscar texto &Search Text miscdialogs&&Seleccin de Hijos&Selection's children miscdialogs Slo en &ttulos &Titles only miscdialogs.Barras de herramien&tas &Toolbars miscdialogsbConsiderar los espacios en blanco como ceros (&0)&Treat blank fields as zeros miscdialogs^&Usar la fuente predeterminada de la aplicacin&Use app default font miscdialogs$&Comprimir Archivo&Use file compression miscdialogsH&Usar fuente por defecto del sistema&Use system default font miscdialogs--Separador-- --Separator-- miscdialogsSe ha producido un error grave. TreeLine podra estar en un estado inestable. Se recomienda guardar cualquier cambio de archivo con otro nombre de archivo y reiniciar TreeLine. La informacin de depuracin que se muestra a continuacin se puede copiar y enviar por correo electrnico a doug101@bellz.org junto con una explicacin de las circunstancias del error. A serious error has occurred. TreeLine could be in an unstable state. Recommend saving any file changes under another filename and restart TreeLine. The debugging info shown below can be copied and emailed to doug101@bellz.org along with an explanation of the circumstances.  miscdialogs*Comandos Dis&poniblesA&vailable Commands miscdialogsCo&ntiene Any &match miscdialogsFFuente por defecto de la aplicacinApp Default Font miscdialogsBorrar Cla&ve Clear &Key miscdialogs(Personalizar FuentesCustomize Fonts miscdialogsDPersonalizar Barra de HerramientasCustomize Toolbars miscdialogs DatosData miscdialogsMen de datos Data Menu miscdialogs:Predeterminado - Texto SimpleDefault - Single Line Text miscdialogs EditarEdit miscdialogsMen Edicin Edit Menu miscdialogs"Fuente del EditorEditor View Font miscdialogsBContrasea del Archivo EncriptadoEncrypted File Password miscdialogsFError - Expresin regular no vlida"Error - invalid regular expression miscdialogs2Error - reemplazo fallidoError - replacement failed miscdialogsFrase co&mpleta F&ull phrase miscdialogs CamposFields miscdialogsArchivoFile miscdialogsMen de Archivo File Menu miscdialogs,Propiedades de ArchivoFile Properties miscdialogs4Almacenamiento de Archivos File Storage miscdialogs FiltroFilter miscdialogs BuscarFind miscdialogs"Buscar &Siguiente Find &Next miscdialogs Buscar &AnteriorFind &Previous miscdialogs&Buscar y reemplazarFind and Replace miscdialogsFormatoFormat miscdialogsMen Formato Format Menu miscdialogsEn to&do Full &data miscdialogsCoinci&de Full &words miscdialogsLManejar nodos sin Campos de Numeracin'Handling Nodes without Numbering Fields miscdialogs AyudaHelp miscdialogsMen Ayuda Help Menu miscdialogsCmo Buscar How to Search miscdialogs>Incluir nodos de nivel superiorInclude top-level nodes miscdialogsTexto E&xactoKey full &words miscdialogs0Clave {0} ya est en usoKey {0} is already used miscdialogs"Atajos de TecladoKeyboard Shortcuts miscdialogsRCdigo de idioma o diccionario (opcional)&Language code or dictionary (optional) miscdialogsIconos Grandes Large Icons miscdialogs$Campos Matemticos Math Fields miscdialogsMover Aba&jo Move &Down miscdialogsMover &ArribaMove &Up miscdialogsCampos de N&odo N&ode Fields miscdialogsNo.menuNo menu miscdialogsxNo se encontraron campos de numeracin en los tipos de datos,No numbering fields were found in data types miscdialogsNodoNode miscdialogs"&Ttulos de Nodos Node &Titles miscdialogsMen de Nodo Node Menu miscdialogs Fuente de SalidaOutput View Font miscdialogs$Expresin re&gularRe&gular expression miscdialogs>Escriba de nuevo la contrasea:Re-Type Password: miscdialogsFLa contrasea reescrita no coincideRe-typed password did not match miscdialogsTRecordar la contrasea durante esta sesin%Remember password during this session miscdialogs Reemplaz&ar Todo Replace &All miscdialogs<Reemplazadas {0} coincidenciasReplaced {0} matches miscdialogs"Reemplazar &TextoReplacement &Text miscdialogs Reserva &nmerosReserve &numbers miscdialogsNodo Principal Root Node miscdialogsDBuscar cadena "{0}". No encontradaSearch string "{0}" not found miscdialogsLBsqueda del texto "{0}" no encontradoSearch text "{0}" not found miscdialogs(Ramas &SeleccionadasSelected &branches miscdialogs.Her&manos seleccionadosSelection's &siblings miscdialogs(&Hijos seleccionadosSelection's childre&n miscdialogsIconos Pequeos Small Icons miscdialogs*Direccin de OrdenadoSort Direction miscdialogs$Mtodo de ordenado Sort Method miscdialogsOrdenar Nodos Sort Nodes miscdialogs:Comprobacin de la ortografa Spell Check miscdialogsJComandos de la &barra de herramientasTool&bar Commands miscdialogs:Tamao Barra de Herramienta&s Toolbar &Size miscdialogsDCantidad de barras de herramientasToolbar Quantity miscdialogsHerramientasTools miscdialogs(Men de Herramientas Tools Menu miscdialogs Fuente del rbolTree View Font miscdialogs,TreeLine - Error GraveTreeLine - Serious Error miscdialogs&Numeracin TreeLineTreeLine Numbering miscdialogsBEscriba la Contrasea para "{0}":Type Password for "{0}": miscdialogs,Escriba la Contrasea:Type Password: miscdialogs:Actualizar Numeracin de NodoUpdate Node Numbering miscdialogs$&Encriptar ArchivoUse file &encryption miscdialogsVerView miscdialogsMen Ver View Menu miscdialogsDnde BuscarWhat to Search miscdialogsQu ordenar What to Sort miscdialogsQu ActualizarWhat to Update miscdialogsVentanaWindow miscdialogsMen Ventana Window Menu miscdialogsVNo se permiten contraseas de longitud cero'Zero-length passwords are not permitted miscdialogs$[Todos los campos] [All Fields] miscdialogs"[Todos los tipos] [All Types] miscdialogs NombreName nodeformatActivar editores de datos cuando el puntero del ratn est encima$Activate data editors on mouse hoveroptiondefaultsApariencia Appearanceoptiondefaults(Grabacin automtica Auto SaveoptiondefaultsZAbrir automticamente el ltimo archivo usado!Automatically open last file usedoptiondefaultsCorreccin de sangrado de hijos (en unidades de altura de fuente) +Child indent offset (in font height units) optiondefaults:Clicar en nodo para renombrarClick node to renameoptiondefaults8Formatos del editor de datosData Editor Formatsoptiondefaults FechasDatesoptiondefaults6Funcionalidades disponiblesFeatures Availableoptiondefaults.Primer da de la semanaFirst day of weekoptiondefaultsViernesFridayoptiondefaultsbSangrado (mejor impresin) Archivos TreeLine JSON)Indent (pretty print) TreeLine JSON filesoptiondefaultsXMinimiza aplicacin a la bandeja del sistema#Minimize application to system trayoptiondefaultslMinutos entre respaldos (seleccione 0 para desactivar)+Minutes between saves (set to 0 to disable)optiondefaults LunesMondayoptiondefaultshCantidad de archivos recientes en el men de archivo(Number of recent files in the file menuoptiondefaults:Nmero de niveles de deshacerNumber of undo levelsoptiondefaultsFAbrir archivos en una nueva ventanaOpen files in new windowsoptiondefaults$Archivos recientes Recent FilesoptiondefaultsEliminar archivos inaccesibles en la vista de archivos recientes'Remove inaccessible recent file entriesoptiondefaults\Renombrar los nodos nuevos cuando sean creadosRename new nodes when createdoptiondefaultsZRestaurar la geometra de la ventana anterior Restore previous window geometryoptiondefaultsrRestaurar estados de vista en rbol de archivos recientes(Restore tree view states of recent filesoptiondefaults SbadoSaturdayoptiondefaults6Mostrar panel de navegacinShow breadcrumb ancestor viewoptiondefaultsPMotrar paneles hijos en la vista derecha#Show child pane in right hand viewsoptiondefaultsFMostrar hijos en la vista de salidaShow descendants in output viewoptiondefaultsFMostrar iconos en la vista de rbolShow icons in the tree viewoptiondefaultstMostrar campos matemticos en la vista de Edicin de Datos&Show math fields in the Data Edit viewoptiondefaultsxMostrar campos de numeracin en la vista de Edicin de Datos+Show numbering fields in the Data Edit viewoptiondefaults&Condicin de InicioStartup ConditionoptiondefaultsDomingoSundayoptiondefaults JuevesThursdayoptiondefaults HorasTimesoptiondefaultslDisponible la opcin de pulsar y arrastrar en el rbolTree drag && drop availableoptiondefaults MartesTuesdayoptiondefaultsLMemoria usada por la opcin "deshacer" Undo MemoryoptiondefaultsMircoles Wednesdayoptiondefaults&Cancelar&Canceloptions&Aceptar&OKoptions^Elija la ubicacin del archivo de configuracin"Choose configuration file locationoptionsPCarpeta del programa (para uso portable)$Program directory (for portable use)optionsZDirectorio de inicio de usuario (recomendado)#User's home directory (recommended)options@Error inicializando la impresoraError initializing printer printdata2TreeLine - Exportar a PDFTreeLine - Export PDF printdataAdvertencia: configuracin de margen no compatible en la impresora actual. Guardar los ajustes?JWarning: Margin settings unsupported on current printer. Save adjustments? printdataAdvertencia: configuracin de tamao de pgina y margen no admitida en la impresora actual. Guardar los ajustes de pgina?]Warning: Page size and margin settings unsupported on current printer. Save page adjustments? printdataAdvertencia: configuracin de tamao de pgina no admitida en la impresora actual. Guardar ajustes?KWarning: Page size setting unsupported on current printer. Save adjustment? printdataA&bajo:&Bottom: printdialogs&Cancelar&Cancel printdialogsH&Dibujar lneas para los nodos hijos&Draw lines to children printdialogs&rbol completo &Entire tree printdialogs&Fuente&Font printdialogs(Seleccin de &Fuente&Font Selection printdialogs&Opciones &Generales&General Options printdialogs*Encabezado I&zquierdo &Header Left printdialogs"&Encabezado y pie&Header/Footer printdialogs*Incluir el nodo rai&z&Include root node printdialogsT&Mantener el primer nodo hijo con el padre&Keep first child with parent printdialogsIz&quierda:&Left: printdialogs&&Nmero de columnas&Number of columns printdialogs&OK&OK printdialogs&Prefijo&Prefix printdialogs&Imprimir... &Print... printdialogsDerec&ha:&Right: printdialogs&Sufijo&Suffix printdialogsA&rriba:&Top: printdialogs&Unidades&Units printdialogs&Usar la misma que haya configurada en "Salida de Datos" de TreeLine&Use TreeLine output view font printdialogsA&nchura:&Width: printdialogs"A3 (279 x 420 mm)A3 (279 x 420 mm) printdialogs"A4 (210 x 297 mm)A4 (210 x 297 mm) printdialogs"A5 (148 x 210 mm)A5 (148 x 210 mm) printdialogs>AaBbCcDdEeFfGg...TtUuVvWvXxYyZzAaBbCcDdEeFfGg...TtUuVvWvXxYyZz printdialogs Centmetros (cm)Centimeters (cm) printdialogsColumnasColumns printdialogs(Tamao Personalizado Custom Size printdialogs*Fuente predeterminada Default Font printdialogsXError: Tamao de pgina o mrgenes invlidos(Error: Page size or margins are invalid printdialogsTexto adicional Extra Text printdialogs&Pginas enfrentadas Facing Pages printdialogsCaractersticasFeatures printdialogsCampo&sFiel&ds printdialogs"For&mato de Campo Field For&mat printdialogs6Formato de Campo para "{0}"Field Format for "{0}" printdialogs Ajuste de pginaFit Page printdialogsAjuste de ancho Fit Width printdialogs"E&stilo de fuente Font st&yle printdialogs &Pie:Foot&er: printdialogsPie Iz&quierdo Footer &Left printdialogsPie ce&ntradoFooter Ce&nter printdialogsPie De&recho Footer Righ&t printdialogs"A&yuda de Formato Format &Help printdialogsEncabe&zado:He&ader: printdialogs&Encabezado &Derecho Header &Right printdialogs(Encabezado cen&tradoHeader C&enter printdialogs4Encabezado y pie de pginaHeader and Footer printdialogsAltura:Height: printdialogsPulgadas (in) Inches (in) printdialogsNodos IncludosIncluded Nodes printdialogsSangrarIndent printdialogsbSangrado de &Salida (unidades de altura de lnea)"Indent Offse&t (line height units) printdialogsPaisa&je Lan&dscape printdialogs(Legal (8.5 x 14 in.)Legal (8.5 x 14 in.) printdialogs(Carta (8.5 x 11 in.)Letter (8.5 x 11 in.) printdialogsMrgenesMargins printdialogsMilmetros (mm)Millimeters (mm) printdialogs Pgina Siguiente Next Page printdialogs2Incluir solo nodos &hijosOnl&y open node children printdialogsOrientacin Orientation printdialogsSalida &FormatoOutput &Format printdialogs0Configuracin de &Pgina Page &Setup printdialogs"Tamao del Pape&l Paper &Size printdialogsRetra&to Portra&it printdialogsPgina anterior Previous Page printdialogsImprimirPrint printdialogs &Vista Previa...Print Pre&view... printdialogs2Vista Previa de Impresin Print Preview printdialogs4Configuracin de Impresin Print Setup printdialogs4Configuracin de ImpresinPrinting Setup printdialogsEjemploSample printdialogs,Seleccionar Im&presoraSelect &Printer printdialogs$Seleccionar Fuente Select Font printdialogs(&Ramas seleccionadasSelected &branches printdialogs(&Nodos seleccionadosSelected &nodes printdialogs&TamaoSi&ze printdialogsPgina sencilla Single Page printdialogs.Espacio entre colu&mnasSpace between colu&mns printdialogs,Tabloide (11 x 17 in.)Tabloid (11 x 17 in.) printdialogs,TreeLine Impresora PDFTreeLine PDF Printer printdialogsQu imprimir What to print printdialogsZoom AumentarZoom In printdialogsZorrm ReducirZoom Out printdialogs&Aadir&Add spellcheck&Cancelar&Cancel spellcheck&Ignorar todo &Ignore All spellcheck&Reemplazar&Replace spellcheck"Aadir &minsculaAdd &Lowercase spellcheckContexto:Context: spellcheckNo se pudo encontrar aspell.exe, ispell.exe o hunspell.exe Buscar la ubicacin?QCould not find either aspell.exe, ispell.exe or hunspell.exe Browse for location? spellcheckTerminada revisin de la rama Continuar desde la parte superior?3Finished checking the branch Continue from the top? spellcheckFComprobacin ortogrfica finalizadaFinished spell checking spellcheckIgnor&arIgnor&e spellcheckXUbique aspell.exe, ipsell.exe o hunspell.exe-Locate aspell.exe, ipsell.exe or hunspell.exe spellcheck2No est en el diccionarioNot in Dictionary spellcheck Programa (*.exe)Program (*.exe) spellcheck Reem&plazar todo Re&place All spellcheck:Comprobacin de la ortografa Spell Check spellcheck"Error ortogrficoSpell Check Error spellcheckSugerencias Suggestions spellcheckHComprobacin ortogrfica de TreeLIneTreeLine Spell Check spellcheckError de comprobacin ortogrfica TreeLine Comprobar que est instalado aspell, ispell o hunspellLTreeLine Spell Check Error Make sure aspell, ispell or hunspell is installed spellcheckPalabra:Word: spellcheck(Seleccionar en rbolSelect in Tree titlelistviewPREDETERMINADODEFAULT treeformats CAMPOFIELD treeformatsARCHIVOFILE treeformatsTipoCampo FieldType treeformatsFormatoFormat treeformats IconoIcon treeformatsTIPOTYPE treeformats&Negrita &Bold Fonttreelocalcontrol&Copiar&Copytreelocalcontrol&Eliminar Nodo &Delete Nodetreelocalcontrol&Separar Clones&Detach Clonestreelocalcontrol&Exportar... &Export...treelocalcontrol$Enlace &Externo...&External Link...treelocalcontrol"&Tamao de Fuente &Font Sizetreelocalcontrol&Sangrar Nodos &Indent Nodetreelocalcontrol&Cursiva &Italic FonttreelocalcontrolMover A&rriba&Move Uptreelocalcontrol&Nueva Ventana &New Windowtreelocalcontrol &Pegar&Pastetreelocalcontrol&Imprimir... &Print...treelocalcontrol&Rehacer&Redotreelocalcontrol,Re&generar Referencias&Regenerate Referencestreelocalcontrol&Renombrar&Renametreelocalcontrol&Guardar&Savetreelocalcontrol0&Establecer Tipo de Nodo&Set Node Typetreelocalcontrol2Corrector &Ortogrfico...&Spell Check...treelocalcontrol&Deshacer&Undotreelocalcontrol4Deshacer Sangrado de &Nodo&Unindent Nodetreelocalcontrol&Aadir Hijo Add &Childtreelocalcontrol:Aadir Ni&vel de Categora...Add Category &Level...treelocalcontrolNAadir nuevo hijo al padre seleccionado Add new child to selected parenttreelocalcontrolPAadir o modificar un enlace web externo!Add or modify an extrnal web linktreelocalcontrolVAadir o modificar un enlace interno a nodo#Add or modify an internal node linktreelocalcontrolRNo es posible expandir sin campos comunes#Cannot expand without common fieldstreelocalcontrol0Campos de las categorasCategory Fieldstreelocalcontrol&Borrar FormatoClear For&mattingtreelocalcontrolZBorrar actual o seleccionado formato de texto)Clear current or selected text formattingtreelocalcontrolFClonar &Todos los Nodos EmparejadosClone All &Matched Nodestreelocalcontrol`Contraer descendientes cuando se fusionen campos&Collapse descendants by merging fieldstreelocalcontrol`Convertir todos los nodos coincidentes en clones&Convert all matching nodes into clonestreelocalcontrol:Convierte ramas {0} en clones"Converted {0} branches into clonestreelocalcontrol<&Copiar Tipos desde Archivo...Copy Types from &File...treelocalcontrolLCopiar la rama o texto al portapapeles(Copy the branch or text to the clipboardtreelocalcontrollCopiar la configuracin desde otro archivo de TreeLine1Copy the configuration from another TreeLine filetreelocalcontrolCor&tarCu&ttreelocalcontrolRCortar la rama o el texto al portapapeles'Cut the branch or text to the clipboardtreelocalcontrolPredeterminadoDefaulttreelocalcontrol@Eliminar los nodos seleccionadosDelete the selected nodestreelocalcontrollSeparar todos los nodos clonados en las ramas actuales+Detach all cloned nodes in current branchestreelocalcontrolhError - No se puede borrar el archivo de respaldo {}'Error - could not delete backup file {}treelocalcontrolJError: no se pudo leer el archivo {0}Error - could not read file {0}treelocalcontrolRError - no se pudo escribir en el archivoError - could not write to filetreelocalcontrolBError - No se pudo escribir en {}Error - could not write to {}treelocalcontrolLExportar el archivo en varios formatos(Export the file in various other formatstreelocalcontroljExportar a PDF con las opciones de impresin actuales+Export to PDF with current printing optionstreelocalcontrol Archivo guardado File savedtreelocalcontrol,&Acoplar por CategoraFlatten &by Categorytreelocalcontrol&Color de &Fuente...Font C&olor...treelocalcontrolForzar actualizacin de todos los tipos condicionales y campos matemticos3Force update of all conditional types & math fieldstreelocalcontrol>Sangrar los nodos seleccionadosIndent the selected nodestreelocalcontrol.Aadir Hermano &DespusInsert Sibling &Aftertreelocalcontrol*Aadir &Hermano AntesInsert Sibling &BeforetreelocalcontrolvAadir los nodos con las categoras por encima de los hijos$Insert category nodes above childrentreelocalcontrolXAadir nuevo hermano despues de la seleccin"Insert new sibling after selectiontreelocalcontrolTAadir nuevo hermano antes de la seleccin#Insert new sibling before selectiontreelocalcontrol$Enlace &Interno...Internal &Link...treelocalcontrol GrandeLargetreelocalcontrolMs grandeLargertreelocalcontrolEl ms grandeLargesttreelocalcontrolMover Aba&jo M&ove Downtreelocalcontrol&Mover al &Principio Move &FirsttreelocalcontrolMover al &Final Move &LasttreelocalcontrolZDesplazar hacia abajo los nodos seleccionadosMove the selected nodes downtreelocalcontroljMover nodos seleccionados para ser los primeros hijos0Move the selected nodes to be the first childrentreelocalcontrolhMover nodos seleccionados para ser los ltimos hijos/Move the selected nodes to be the last childrentreelocalcontrol\Desplazar hacia arriba los nodos seleccionadosMove the selected nodes uptreelocalcontrol4No existen nodos idnticosNo identical nodes foundtreelocalcontrolRAbrir nueva ventana para el mismo archivo#Open a new window for the same filetreelocalcontrol<Config&uracin de Impresin...P&rint Setup...treelocalcontrol0Pegar Texto sin &FormatoPa&ste Plain TexttreelocalcontrolPegar &Hijo Paste C&hildtreelocalcontrol&Pegar Hijo C&lonadoPaste Cl&oned Childtreelocalcontrol8Pegar Hermano Clo&nado AntesPaste Clo&ned Sibling Beforetreelocalcontrol<Pegar Hermano Clonad&o DespusPaste Clone&d Sibling Aftertreelocalcontrol,Pegar Hermano Despu&sPaste Sibling &Aftertreelocalcontrol(Pegar Hermano &AntesPaste Sibling &BeforetreelocalcontrolPPegar hijo clonado desde el portapapeles&Paste a child clone from the clipboardtreelocalcontrolPPegar un nodo hijo desde el portapapeles%Paste a child node from the clipboardtreelocalcontrolPPegar un hermano despus de la seleccinPaste a sibling after selectiontreelocalcontrolFPegar hermano antes de la seleccin Paste a sibling before selectiontreelocalcontrolZPegar un clon hermano despus de la seleccin%Paste a sibling clone after selectiontreelocalcontrolVPegar un clon hermano antes de la seleccin&Paste a sibling clone before selectiontreelocalcontrolRPegar nodos o texto desde el portapapeles&Paste nodes or text from the clipboardtreelocalcontrolPPegar texto sin formato del portapapeles+Paste non-formatted text from the clipboardtreelocalcontrol$Imprimir a PD&F...Print &to PDF...treelocalcontrol:&Vista Previa de Impresin...Print Pre&view...treelocalcontrolpImprimir salida de rbol en base a las opciones actuales*Print tree output based on current optionstreelocalcontrol&Propiedades...Prop&erties...treelocalcontrolzRehacer la ltima accin sobre la que se ha aplicado deshacerRedo the previous undotreelocalcontrolTRenombrar ttulo completo del rbol en uso#Rename the current tree entry titletreelocalcontrolD&Intercambiar Niveles de CategoraS&wap Category Levelstreelocalcontrol Guardar &Como... Save &As...treelocalcontrolGuardar archivo Save Filetreelocalcontrol.Guardar cambios en {}?Save changes to {}?treelocalcontrol"Guardar cambios? Save changes?treelocalcontrol2Guardar el archivo actualSave the current filetreelocalcontrolLGuardar el archivo con un nombre nuevoSave the file with a new nametreelocalcontrolFSeleccionar campos para nuevo nivelSelect fields for new leveltreelocalcontrol0Definir tamao de fuente Set Font Sizetreelocalcontrol.Establecer tipo de nodo Set Node Typetreelocalcontrol~Establecer parmetros de archivo como compresin y encriptacin3Set file parameters like compression and encryptiontreelocalcontrolEstablecer mrgenes, tamao de pgina y otras opciones de impresin1Set margins, page size and other printing optionstreelocalcontrolhEstablecer el tamao del texto actual o seleccionado(Set size of the current or selected texttreelocalcontrolfEstablecer el color del texto actual o seleccionado-Set the color of the current or selected texttreelocalcontroljEstablecer la fuente actual o seleccionada en negrita(Set the current or selected font to boldtreelocalcontroljEstablecer la fuente actual o seleccionada en cursiva*Set the current or selected font to italictreelocalcontrolpEstablecer la fuente actual o seleccionada para subrayar-Set the current or selected font to underlinetreelocalcontrolhEstablecer tipo de nodo para los nodos seleccionados$Set the node type for selected nodestreelocalcontrolVVista previa de los resultados de impresin"Show a preview of printing resultstreelocalcontrolPequeoSmalltreelocalcontrolHRevisar los datos de texto del rbol Spell check the tree's text datatreelocalcontrolVIntercambiar categora nodos hijos y nietos(Swap child and grandchild category nodestreelocalcontrolRTreeLine - Abrir archivo de configuracin"TreeLine - Open Configuration Filetreelocalcontrol.TreeLine - Guardar comoTreeLine - Save Astreelocalcontrol&SubrayadoU&nderline Fonttreelocalcontrol6Deshacer la accin anteriorUndo the previous actiontreelocalcontrolXEliminar sangrado de los nodos seleccionadosUnindent the selected nodestreelocalcontrolAdvertencia: archivo corrupto! Se omitieron referencias de elementos secundarios incorrectos en los siguientes nodos:OWarning - file corruption! Skipped bad child references in the following nodes:treelocalcontrol,&Acerca de TreeLine...&About TreeLine...treemaincontrolUso &Bsico...&Basic Usage...treemaincontrol2&Cancelar archivo abierto&Cancel File Opentreemaincontrol0Bsqueda &Condicional...&Conditional Find...treemaincontrol:Configurar Tipos de &Datos...&Configure Data Types...treemaincontrol8Eliminar copia de segurida&d&Delete Backuptreemaincontrol &Buscar Texto... &Find Text...treemaincontrol4&Documentacin completa...&Full Documentation...treemaincontrol,Opciones &Generales...&General Options...treemaincontrolImporta&r... &Import...treemaincontrol&Nuevo...&New...treemaincontrol&Abrir...&Open...treemaincontrol &Salir&Quittreemaincontrol:&Restaurar copia de seguridad&Restore Backuptreemaincontrol"Seleccionar T&odo &Select Alltreemaincontrol(&Seleccionar Ejemplo&Select Sampletreemaincontrol,&Seleccionar Plantilla&Select Templatetreemaincontrol&Filtro de &Texto...&Text Filter...treemaincontrolExiste un archivo de respaldo: "{}". Una sesin anterior puede haberse bloqueadoAbrir un archivo desde el discoOpen a file from disktreemaincontrol<Abriri un arhchivo no-TreeLineOpen a non-TreeLine filetreemaincontrol6Abrir un archivo de ejemploOpen a sample filetreemaincontrolXReemplazar cadenas de texto en datos de nodo!Replace text strings in node datatreemaincontrolLSeleccionar todo el texto en un editorSelect all text in an editortreemaincontrol@Establecer &Atajos de Teclado...Set &Keyboard Shortcuts...treemaincontrolfConfigurar las preferencias para todos los archivos"Set user preferences for all filestreemaincontrolNMostrar Confi&guracin de Estructura... Show C&onfiguration Structure...treemaincontrolxMostrar visualizacin de solo lectura del tipo de estructura.Show read-only visualization of type structuretreemaincontrol"&Ordenar Nodos...Sor&t Nodes...treemaincontrol2Comenzar un archivo nuevoStart a new filetreemaincontrol0TreeLine - Abrir ArchivoTreeLine - Open Filetreemaincontrol&TreeLine Uso BsicoTreeLine Basic Usagetreemaincontrol(TreeLine version {0}TreeLine version {0}treemaincontrol2Actualizar &Numeracin...Update &Numbering...treemaincontrolPActualizar campos de numeracin de nodosUpdate node numbering fieldstreemaincontrolXUsar condiciones de campo para filtrar nodos$Use field conditions to filter nodestreemaincontrolZUse condiciones de campo para encontrar nodos"Use field conditions to find nodestreemaincontrolZAdvertencia: no se pudo crear el socket local'Warning: Could not create local sockettreemaincontrol0Directorio no encontradomissing directorytreemaincontrolescrito por {}written by {0}treemaincontrol NuevoNewtreenodePrincipalMain treestructureVFiltrado condicional, encontrados {0} nodos&Conditional filtering, found {0} nodestreeviewTFiltrando por "{0}", encontrados.{1} nodos#Filtering by "{0}", found {1} nodestreeviewSiguiente: {0} Next: {0}treeview>Siguiente: {0} (no encontrado)Next: {0} (not found)treeviewBuscar: Search for:treeviewBuscar {0}Search for: {0}treeview6Buscar {0} (no encontrado)Search for: {0} (not found)treeview&Cerrar Ventana &Close Window treewindow*&Reducir Toda la Rama&Collapse Full Branch treewindow &Datos&Data treewindow&Editar&Edit treewindow,&Expandir Toda la Rama&Expand Full Branch treewindow&Archivo&File treewindow A&yuda&Help treewindow(Seleccin &Siguiente&Next Selection treewindow &Nodo&Node treewindow"Seleccin &Previa&Previous Selection treewindow&Mostrar Panel &Hijo&Show Child Pane treewindow&Herramientas&Tools treewindow&Ver&View treewindowVen&tana&Window treewindow&Cerrar esta ventanaClose this window treewindowdReducir todos los hijos de los nodos seleccionados+Collapse all children of the selected nodes treewindowEditor de Datos Data Edit treewindowSalida de Datos Data Output treewindowfExpandir todos los hijos de los nodos seleccionados)Expand all children of the selected nodes treewindow&FormatoFo&rmat treewindowhIr a la siguiente seleccin de rbol en el historial(Go to the next tree selection in history treewindow<Bsqueda incremental siguienteNext Incremental Search treewindow:Bsqueda incremental anteriorPrevious Incremental Search treewindowNVolver a la seleccin de rbol anterior%Return to the previous tree selection treewindowDMostrar &vista panel de navegacinShow &Breadcrumb View treewindow2Mostrar &Lista de TtulosShow &Title List treewindow0Mostrar Editor de &DatosShow Data &Editor treewindow0&Mostrar Salida de DatosShow Data &Output treewindowPMostrar Salida de Datos y Des&cendientesShow Output &Descendants treewindowhMostrar el editor de datos en la vista de la derechaShow data editor in right view treewindow\Mostrar la salida de datos en la vista derechaShow data output in right view treewindowjMostrar la lista de ttulos en la vista de la derechaShow title list in right view treewindow8Iniciar bsqueda incrementalStart Incremental Search treewindow Lista de Ttulos Title List treewindow^Alternar mostrando vista anterior de navegacin'Toggle showing breadcrumb ancestor view treewindow|Alternar mostrando la vista de salida sangra de descendients/Toggle showing output view indented descendants treewindow:Alternar mostrando resultados%Toggle showing right-hand child views treewindowTreeLine/translations/treeline_zh.qm0000644000175000017500000021735013715363644016657 0ustar dougdougYg#hDm'ex,I[&HXrulHKv%wDx0z^y^70Ke78OH5H5fU~VE9c SEXh5X,Se"D0K5[R7~8&Q* %*^*y***%K*TI*6%*0{*5_ +5t+U+++m++8A+:+įR+/55S4 ;0|c nE_%v{-+6nd6n[6n;Y =!>6OGSR5-_ c gx?|`-ۇR*W%GZ >wi|eJ~ |g_~ҮN%| <~C JuwL 4% %Lom*tBU\~~#_.E1SÅ9ځ9@b D"NBN$I!R fXMP[ó*aLJa2f@ggli 5lwploFs,z6}'~ov.t?dn@B@{bFJ\8R``}Cf4gNehT?yh!rGIx*3z}+G`NcB%G%53cCIN%JcT(NK.Fakz+%"DuϑMĮeT )ל5jשD"Kؙ<:Kg?kH]Sh!Pmym/Ns t d#[IѤcy1_~nhUb#C9#Cb&$͕*,+. //_7/y0?mIn3\L RSS"kZzZ]_Pb#pCe,ffȊ7gGlr^PhOnZ:ITi$ ~|&ϥ&K@.o.}R@i˃~"Ψa4`7ie 7Qx_qE>TT&4ҀY7 .|&k 3&t\(m36)*/}O0nd7OF]8OFCd_P[R99VSU۾U]15hEkkNn?ninUp\{}\<nDo`?Nߠue/1/ [2Ǩ_˛tOe^"ufkE3vj^8fOZųA - '*Sey,0 1cD{1cDF9H`@EamFytLC\W$:Yh[%ZH]!bbn#,wƾyYB{0>6P |9[cc_pcyMHnjTz_ ©PMq*)VH9Po&t^ =%%ns%%`xA)̇$,q%oJU>EԤ?VK޹߂3ߗevu+a ^Ym ҷb r  \~ \~ ;) q#! - ! 4Ed 5 :rN F LV MgZ f+>G kY dT 81  " ωiK \H j bbH { b*5 ld7 W ߡX  =  , p ʵ tKa n# O eI  L) f cY V Ӆ .j TQ T]e  W(X ,_$$i /& 4 5ru 7* =! ITE Ty U Wܾ)2 _҉ ae c5< cP e uo~ u5 w5mw ăD 52 1y 3\~ : I I I I0I Ik I I I CS 8r 9G /  ,0 6" 5  ' ^& I9 - & t ˈ ĽNR ;: Gc : :lZ 5 l  ; 5 e3H {O 9 s|Q .n.  to~ X K2v )M -8Z .2ּ /. 2  5# 93 @(e AN Ks3 KI L.\ L9W W X [tJ pI3[y rWh x( x( {u@ :3 jX sm?  HQ  ; O Z/ i" -? D7 (c ]Pr 8  D Ip B ~p t0 ٷm ߺc KG ~ \E K ^Ѱ y~ɒ + vh w A E 2B +0 P % #, Q lJ G # '{ (E3 * s 2l 7"!V 8ǑR ;y* =` @|5p @ V dMK g#% n/ pki tf tc tU v" {Bqd =7j e_ hl MC T6   CK 4p ev m)1 ү v bnF D Ux Y\ \ -w 3X  'i . 0u9 0 3T 7y* C G9.h H Kz9a Ki M S NK# ]`^JR _ T dT i'9 i$^ i$l m. z  27  ݃y |1" =$ cE DD ˜S! D^ X s9 ~t > st :I,Y dcx"<sIs=(tz$(Y-z/+!0/E0>EN9 = EjH;\L`L^OL[JL^V(Y Zrhj8i?Oklfcp#4#qtt/C[Dbx^Ae-STtSybUte:Kk\D-`E1ԕXՁYe#9fh\ tppVCu.~`n;> jKc R(x68 qV<s@-HNI0|MU Z8n[Ke3em3:|iFW.rxs$§ww |*}$7!?y A ӝԲ) Q']t2IMmU4nc#HMY3՜u"$&$| ɟ)N~#]-jo+hoDrds7Q3]%6fWCi Sm(&C)&Cancelcolorset xn(&O)&OKcolorsetr_iN; Color ThemecolorsetmReR(&A) &Add New Rule conditional Sm(&C)&Cancel conditional Qs(&C)&Close conditional R d(&D)&Delete conditional~g_n(&E) &End Filter conditional n(&F)&Filter conditional }Qe(&L)&Load conditional xn(&O)&OK conditionalR dR(&R) &Remove Rule conditional O[X(&S)&Save conditionalPGFalse conditionalgb~N NN*(&N) Find &Next conditionalgb~N NN*(&P)Find &Previous conditionalT [W:Name: conditionallg b~R0gaNS9M!No conditional matches were found conditionalp|{W Node Type conditional R{0}Rule {0} conditionalO[XR Saved Rules conditionalwTrue conditional [b@g |{W] [All Types] conditionalNand conditionalST+contains conditional N...~\> ends with conditionalbor conditional N..._Y starts with conditional ^u((&A)&Apply configdialog Sm(&C)&Cancel configdialogepcn|{W(&D) &Data Type configdialogR d|{W(&D) &Delete Type configdialogNSge|{WMu(&D)&Derive from original configdialogez _(&E) &Equation configdialog[WkMn(&F) &Field Config configdialog[Wk|{W(&F) &Field Type configdialogϚ~(&H)&Hide Advanced configdialog N y(&M) &Move Down configdialoge[Wk(&N)... &New Field... configdialoge|{W(&N)... &New Type... configdialog xn(&O)&OK configdialog RM(&P)&Prefix configdialog n(&R)&Reset configdialog~g|{W(&R) &Result Type configdialog bb@g (&S) &Select All configdialogf>y:~(&S)&Show Advanced configdialogc^hQ(&S)&Sort Criteria configdialoghh<_(&T) &Title Format configdialogW(pNKmRzzL(&b)Add &blank lines between nodes configdialogmR[Wk Add Field configdialogmR|{WAdd Type configdialogmRbR depcn|{WAdd or Remove Data Types configdialogmReg,yv{&S(&s)Add text bullet&s configdialogQA&HTMLYh<_eg,Allow &HTML rich text in format configdialog {g/{{&Arithmetic Operators configdialogR|{WAutomatic Types configdialogSu([WkRh(&F)Available &Field List configdialogSu([Wk(&F)Available &Fields configdialog^\~gBoolean Result configdialogelR dpOu(vepcn|{W+Cannot delete data type being used by nodes configdialoge9SVh(&I) Change &Icon configdialog[Pep Child Count configdialog[P_u(Child Reference configdialog [P|{WPR6Child Type Limits configdialognd b(&S) Clear &Select configdialogb|{W(&p)... Co&py Type... configdialog ~T&&[PRhQR{&(&S)+Combination && Child List Output &Separator configdialog kԏ{{&Comparison Operators configdialog Mnepcn|{WConfigure Data Types configdialogY R6|{W Copy Type configdialogepCount configdialogR^gaN|{W(&n)Create Co&nditional Types configdialogeg~g Date Result configdialogepv:wP<(&V)Default &Value for New Nodes configdialog؋[Pp|{W(&T)Default Child &Type configdialog [NIez _Define Equation configdialog[NIep[f[Wkez Define Math Field Equation configdialogR d[Wk(&t) Dele&te Field configdialogNΐu(|{WMu(&G)Derived from &Generic Type configdialogcϏ Description configdialogeT Direction configdialog Vh^ Editor Height configdialogQeev[WkT y:Enter new field name: configdialogQee|{WvT [W:Enter new type name: configdialogez _{}Equation error: {} configdialog  - ep[fW:ez N-v_s_u(2Error - circular reference in math field equations configdialogO0&HTMLhEvaluate &HTML tags configdialogYe[W Extra Text configdialog [Wk(&i)F&ield configdialog[WkRh(&i) F&ield List configdialog[WkField configdialog[WkRh(&L) Field &List configdialog[Wk_u(Field References configdialog eNO`oS€File Info Reference configdialogleT(&D)Flip &Direction configdialogh<_^.R(&H) Format &Help configdialogVhIcon configdialog ep[fez _ Math Equation configdialogOe9[WkRh(&F)Modify &Field List configdialogOe9gaN|{W(&n)Modify Co&nditional Types configdialog N y(&U)Move &Up configdialog N y(&w) Move Do&wn configdialog N y(&p)Move U&p configdialogT [WName configdialogeNone configdialogeg,Lep(&b)Num&ber of text lines configdialogep[W~g Number Result configdialog [PpepNumber of Children configdialog{|{W(&p)O&perator Type configdialog Q(&u)O&utput configdialog{Rh(&a)Oper&ator List configdialog{ Operations configdialog QvN[WkS€Other Field References configdialogQh<_(&p)Out&put Format configdialogQh<_(&t)Outpu&t Format configdialog QHTML Output HTML configdialogQ yOutput Options configdialogr6_u(Parent Reference configdialogS€|{W(&n)Refere&nce Type configdialog_u(~R+(&L)Reference &Level configdialog_u(|{W(&T)Reference &Type configdialogS€~R+(&v)Reference Le&vel configdialogT}T [Wk(&m)...Rena&me Field... configdialogT}T |{W(&m)...Rena&me Type... configdialog T}T [Wk Rename Field configdialog T}T |{W Rename Type configdialogN{}T}T N::Rename from {} to: configdialogh9_u(Root Reference configdialogN (&N) Select &None configdialog_u(Self Reference configdialognepcn|{WVhSet Data Type Icon configdialogg gaNW0n|{WSet Types Conditionally configdialogc^.(&K)... Sort &Keys... configdialogc^.Sort Key configdialog c^Qs.[WkSort Key Fields configdialog T(&x)Suffi&x configdialog|{WRh(&y) T&ype List configdialog eg,{{&Text Operators configdialogeg,~g Text Result configdialogN QAOu(NN [W{&{},The following characters are not allowed: {} configdialog T yN N:zzThe name cannot be empty configdialogT yN ST+zzh<The name cannot contain spaces configdialogT yN N xml _Y4 The name cannot start with "xml" configdialogT y_Ř{N[Wk_Y4!The name must start with a letter configdialogT y]Ou(The name was already used configdialoge~g Time Result configdialog|{WMn(&e) Typ&e Config configdialog|{WType configdialogN:[WkepcnOu(hh<(&d)Use a table for field &data configdialog[b@g Su(|{W][All Types Available] configdialog[e][None] configdialog~[P<absolute value configdialogRadd configdialogSOY_& arc cosine configdialogkc_&larc sine configdialogSkcR arc tangent configdialog^sWGaverage configdialogN10N:^v[epbase-10 logarithm configdialogceg,concatenate text configdialog\eg,lcbN:\Qconvert text to lower case configdialog\eg,lcbN:Y'Qconvert text to upper case configdialogOY_&cosine of radians configdialog ^epR0_'^degrees to radians configdialogddivide configdialog{INequal to configdialog6NX factorial configdialogmnpfloating point configdialog TN Setd floor divide configdialogkcTforward configdialogkcTfwd configdialogY'N greater than configdialogY'N{INgreater than or equal to configdialog*fvetep(higher integer)higher integer configdialog,W({,NN*SepN- \{,NN*SepfcbN:{,N N*Sep(in 1st arg, replace 2nd arg with 3rd arg configdialog Ou({,NN*SepO\N:R{&ceg,$join text using 1st arg as separator configdialog\N less than configdialog\N{INless than or equal to configdialog;N logical and configdialog;b logical or configdialog(ONvetep(lower integer) lower integer configdialoggY'maximum configdialogg\minimum configdialog|epmodulus configdialogNXmultiply configdialog q6[ep^8epnatural log constant configdialogq6[epnatural logarithm configdialogN {I not equal to configdialog^8ep pi constant configdialogNXepower configdialog_'^R0^radians to degrees configdialogTrev configdialogTreverse configdialogVۂ NQeround to num digits configdialogkc_&sine of radians configdialog^seh9 square root configdialogQsubtract configdialogyv`;T sum of items configdialog,R~(tangent of radians)tangent of radians configdialog(Yg{,NN*eg,SepST+{,NN*Sep RN:w%true if 1st text arg contains 2nd arg configdialog*Yg{,NN*eg,SepN{,NN*Sep~\> RN:w&true if 1st text arg ends with 2nd arg configdialog*Yg{,NN*eg,SepN{,NN*Sep_Y4 RN:w(true if 1st text arg starts with 2nd arg configdialogwP<, gaN, PGP<"true value, condition, false value configdialog0b*evetep(truncated integer)truncated integer configdialogmOeN(&B)&Browse for File dataeditors Sm(&C)&Cancel dataeditorslR0vh(&G) &Go to Target dataeditors xn(&O)&OK dataeditorsbS_c(&O) &Open Link dataeditorsbS_VrG(&O) &Open Picture dataeditors(W(hN-pQcvh)(Click link target in tree) dataeditors~[Absolute dataeditorsW0W@Address dataeditorsndc(&L) Clear &Link dataeditorsf>y:T y Display Name dataeditorsYc External Link dataeditors eN_|{WFile Path Type dataeditorsQc Internal Link dataeditorsbS_eNY9(&F) Open &Folder dataeditorsVrGc Picture Link dataeditorsv[Relative dataeditorsehHScheme dataeditorsnN:_SRMe(&N) Set to &Now dataeditors_SRMeg(&D) Today's &Date dataeditors"TreeLine - YceNTreeLine - External Link File dataeditorsTreeLine - VrGeNTreeLine - Picture File dataeditorsclink dataeditors R(&C)&Columnsexports.SR(CSV)TNhh<(~R+S)(&C);&Comma delimited (CSV) table of descendants (level numbers)exportsethh(&E) &Entire treeexports &HTML&HTMLexports&HTMLh<_vNf{~&HTML format bookmarksexportsSbh9p(&I)&Include root nodesexports &ODFY'~ &ODF Outlineexports.rHg,TreeLine (2.0.x)(&O)&Old TreeLine (2.0.x)exportsSg bS_pv[Pp(&O)&Only open node childrenexportsSUN*HTMLub(&S)&Single HTML pageexportsh{~he[W(&T)&Tabbed title textexports eg,(&T)&TextexportsTreeLine[Ph(&T)&TreeLine Subtreeexportsg*h<_Svb@g eg,Q(&U)&Unformatted output of all textexports&XBELh<_vNf{~&XBEL format bookmarksexports&XML (u()&XML (generic)exports Nf{~(&m) Book&marksexportsNf{~ Bookmarksexports b[Qh<_[P|{WChoose export format subtypeexports b[Qh<_|{WChoose export format typeexports b[Q yChoose export optionsexports(SR(CSV)[Phh<(SU~)(&d)7Comma &delimited (CSV) table of children (single level)exportsD - elՔcR0g*O[XvTreeLineeN0 O[XeN^v͋0FError - cannot link to unsaved TreeLine file. Save the file and retry.exports: - g*b~R0[Qj!geN0 hgTreeLine[0JError - export template files not found. Check your TreeLine installation.exportseN[Q File ExportexportsSbbSSphTu(&p)Include &print header && footerexports:[ehV cR0TreeLineeN(u(NWebg RVh)8Live tree view, linked to TreeLine file (for web server)exports [ehV SUN*eN(]LQeepcn)+Live tree view, single file (embedded data)exportsYN*HTMLTepcnh(&d)Multiple HTML &data tablesexports$^&[*zh!Digit or Space (external)  fieldformatpR{& \.Dot Separator \. fieldformat eg,~\> $ End of Text $ fieldformatryk[W{&lNI \Escape a Special Character \ fieldformatO[P 1/2/3/4Example 1/2/3/4 fieldformatcep (Y'Q) EExponent (capital) E fieldformatcep(\Q) eExponent (small) e fieldformatYc ExternalLink fieldformat6\e(0-23, 1 or 2 digits) %-HHour (0-23, 1 or 2 digits) %-H fieldformat,\e(00-23, 2 digits) %HHour (00-23, 2 digits) %H fieldformat.\e (01-12, 2 digits) %IHour (01-12, 2 digits) %I fieldformat8\e (1-12, 1 or 2 digits) %-IHour (1-12, 1 or 2 digits) %-I fieldformat Htmleg,HtmlText fieldformatQc InternalLink fieldformat ~R{& /Level Separator / fieldformat\Q[Wk [a-z]Lower Case Letters [a-z] fieldformatep[fMath fieldformat ky (6 digits) %fMicroseconds (6 digits) %f fieldformat*R (1 or 2 digits) %-MMinute (1 or 2 digits) %-M fieldformatR (2 digits) %MMinute (2 digits) %M fieldformat*g (1 or 2 digits) %-mMonth (1 or 2 digits) %-m fieldformatg (2 digits) %mMonth (2 digits) %m fieldformat g)Q %bMonth Abbreviation %b fieldformat gT [W %B Month Name %B fieldformat^ep[W [^0-9]Not a Number [^0-9] fieldformatsW(Now fieldformatep[WNumber fieldformatep 1Number 1 fieldformatS Numbering fieldformatSULeg, OneLineText fieldformat S ep[W #Optional Digit # fieldformat S h_ -Optional Sign - fieldformatb |Or | fieldformat,Y'~O[P I../A../1../a)/i)!Outline Example I../A../1../a)/i) fieldformatVrGPicture fieldformat kcRh_RegularExpression fieldformat _ŗep[W 0Required Digit 0 fieldformat_ŗvh_ +Required Sign + fieldformat*y (1 or 2 digits) %-SSecond (1 or 2 digits) %-S fieldformaty (2 digits) %SSecond (2 digits) %S fieldformatk=O[P 1.1.1.1Section Example 1.1.1.1 fieldformatk=R{& .Section Separator . fieldformat R{& / Separator / fieldformatep[W [0-9]Set of Numbers [0-9] fieldformat \Q[Wk aSmall Letter a fieldformat\QWlep[W iSmall Roman Numeral i fieldformat$zzh"Space Separator (internal)  fieldformateg, SpacedText fieldformatT/FT/F fieldformateg,Text fieldformateTime fieldformatY'Q[Wk [A-Z]Upper Case Letters [A-Z] fieldformat$fgep[W (0 to 53) %-UWeek Number (0 to 53) %-U fieldformatfg)Q %aWeekday Abbreviation %a fieldformatfgT [W %AWeekday Name %A fieldformatY/NY/N fieldformat^t (2 digits) %yYear (2 digits) %y fieldformat^t (4 digits) %YYear (4 digits) %Y fieldformattrue/false true/false fieldformat yes/noyes/no fieldformatPGfalse genbooleanT&no genbooleanwtrue genbooleanf/yes genbooleanb@g eN All Files globalrefb@g TreeLineeNAll TreeLine Files globalrefCSV(SR)eNCSV (Comma Delimited) Files globalref HTMLeN HTML Files globalrefODFeg,eNODF Text Files globalrefTreeLineeNOld TreeLine Files globalref PDFeN PDF Files globalrefeg,eN Text Files globalrefTreeLineeNTreeLine Files globalrefTreeLineeN - S)TreeLine Files - Compressed globalrefTreeLineeN - R[TreeLine Files - Encrypted globalrefTreepadeN Treepad Files globalref XMLeN XML Files globalrefgb~ Find: helpview V(&B)&Backhelpview RM(&F)&Forwardhelpview N;u(&H)&Homehelpviewgb~TNN*(&N) Find &Nexthelpviewgb~RMNN*(&P)Find &Previoushelpview lb~R0eg,N2Text string not foundhelpview]QwToolshelpview>"{0}"N f/g eHvTreeLineeN0 Ou([QenVh:"{0}" is not a valid TreeLine file. Use an import filter?imports,u(XML(^TreeLineeN)(&G) &Generic XML (non-TreeLine file)imports$&HTMLNf{~(Mozillah<_) &HTML bookmarks (Mozilla Format)imports&Tab)eg, kψLNN*p%&Tab indented text, one node per lineimports&XMLNf{~(XBELh<_)&XML bookmarks (XBEL format)importsNf{~BOOKMARKimportsL{0}N vCSVh<_Bad CSV format on Line {0}importsNf{~ Bookmarksimports b[Qee_Choose Import Methodimports<^&g ~R+R && hLvSR(CSV)eg,hh<(&m)ACo&mma delimited (CSV) text table with level column && header rowimports.^&g hLvSR(CSV)eg,hh<(&w)1Comma delimited (CSV) text table &with header rowimports - elՋSeN{0}Error - could not read file {0}imports  - {0}N-vh<_N kcxnError - improper format in {0}importsv_UFOLDERimports[QeeN Import Fileimports eeHveN Invalid Fileimports{,{0}LN v~R+SeeH Invalid level number on line {0}imports ~R+~geeHInvalid level structureimportscLinkimports.rHg,Tree&LineeN(1.xb2.x)Old Tree&Line File (1.x or 2.x)imports,Open &Document (ODF)Y'~Open &Document (ODF) outlineimportsQvNOtherimports"~eg,Nk=(zzv}LR)(&p)-Plain text ¶graphs (blank line delimited)imports(~eg, kψLNN*p(VޏfR)(&n)-Plain text, one &node per line (CR delimited)importsR{& SEPARATORimportshh<TABLEimports&^&hTLvTabReg,hh<(&r))Tab delimited text table with header &rowimportseg,TextimportsL{0}N vgavY*YToo many entries on Line {0}importsTreeLine - [QeeNTreeLine - Import Fileimports*TreepadeN(NŖPeg,p)(&f)Treepad &file (text nodes only)imports_Ř{W(QepN-~T[P_u(/Child references must be combined in a functionmatheval^lv"{}"[W{&Illegal "{}" charactersmatheval[XW(^lQep: {0}Illegal function present: {0}matheval^l[a|{Wb{{&: {0}$Illegal object type or operator: {0}matheval{I_N-v^lՋlIllegal syntax in equationmatheval ^u((&A)&Apply miscdialogs Sm(&C)&Cancel miscdialogs Qs(&C)&Close miscdialogs~g_n(&E) &End Filter miscdialogsethh(&E) &Entire tree miscdialogs n(&F)&Filter miscdialogsgb~N NN*(&F) &Find Next miscdialogs kcT(&F)&Forward miscdialogs_ue^v(&I)&Ignore and skip miscdialogsQs.(&K) &Key words miscdialogsp|{W(&N) &Node Type miscdialogs xn(&O)&OK miscdialogs[NIvQs.[Wk(&P)&Predefined Key Fields miscdialogskcRh_(&R)&Regular expression miscdialogs fcb(&R)&Replace miscdialogs T/N NN*T ~pvS(&R)"&Restart numbers for next siblings miscdialogs`bY ؋(&R)&Restore Defaults miscdialogs ST(&R)&Reverse miscdialogsd}"e[W(&S) &Search Text miscdialogs b邂pv[Pp(&S)&Selection's children miscdialogsSg h(&T) &Titles only miscdialogs]Qwh(&T) &Toolbars miscdialogs\zzv}[WkN:(&T)&Treat blank fields as zeros miscdialogsOu(^u(؋[WOS(&U)&Use app default font miscdialogsOu(eNS)(&U)&Use file compression miscdialogsOu(|~ߞ؋[WOS(&U)&Use system default font miscdialogs - R{& -  --Separator-- miscdialogsSuNN%͕0 TreeLineSYNN z3[r`. ^W(SNN*eNT N O[XNOUeNfe9^veT/RTreeLine. N bf>y:vO`oSNY R6^vu5[PNSѐR0doug101@bellz.org [`Qv.A serious error has occurred. TreeLine could be in an unstable state. Recommend saving any file changes under another filename and restart TreeLine. The debugging info shown below can be copied and emailed to doug101@bellz.org along with an explanation of the circumstances.  miscdialogsSu(T}N(&v)A&vailable Commands miscdialogsNaS9M(&m) Any &match miscdialogs ^u(؋[WOSApp Default Font miscdialogsnd.(&K) Clear &Key miscdialogs [NI[WOSCustomize Fonts miscdialogs [NI]QwhCustomize Toolbars miscdialogsepcnData miscdialogsepcnSU Data Menu miscdialogs؋ - SULe[WDefault - Single Line Text miscdialogsEdit miscdialogsSU Edit Menu miscdialogsVhV[WOSEditor View Font miscdialogs R[eN[xEncrypted File Password miscdialogs - eeHvkcRh_"Error - invalid regular expression miscdialogs - fcbY1%Error - replacement failed miscdialogs[etvw(&u) F&ull phrase miscdialogs[WkFields miscdialogseNFile miscdialogseNSU File Menu miscdialogseN\^`'File Properties miscdialogseN[XP File Storage miscdialogsnFilter miscdialogsgb~Find miscdialogsgb~N NN*(&N) Find &Next miscdialogsgb~N NN*(&P)Find &Previous miscdialogs gb~TfcbFind and Replace miscdialogsh<_Format miscdialogsh<_SU Format Menu miscdialogs[etepcn(&d) Full &data miscdialogs[etv(&w) Full &words miscdialogsYtlg S[Wkvp'Handling Nodes without Numbering Fields miscdialogs^.RHelp miscdialogs^.RSU Help Menu miscdialogsYOUd}" How to Search miscdialogs Sbv~pInclude top-level nodes miscdialogs[etvQs.(&w)Key full &words miscdialogs.{0}]Ou(Key {0} is already used miscdialogs .v_cw.Keyboard Shortcuts miscdialogsNxb [WQx(S )&Language code or dictionary (optional) miscdialogsY'Vh Large Icons miscdialogsep[f[Wk Math Fields miscdialogs N y(&D) Move &Down miscdialogs N y(&U)Move &Up miscdialogsp[Wk(&o) N&ode Fields miscdialogseSUNo menu miscdialogsW(epcn|{WN-b~N R0S[Wk,No numbering fields were found in data types miscdialogspNode miscdialogsph(&T) Node &Titles miscdialogspSU Node Menu miscdialogs QV[WOSOutput View Font miscdialogskcRh_(&g)Re&gular expression miscdialogseQe[x:Re-Type Password: miscdialogseQev[xN S9MRe-typed password did not match miscdialogsW(kdOgOO[x%Remember password during this session miscdialogsfcbQh(&A) Replace &All miscdialogsfcbN{0}N*S9MyReplaced {0} matches miscdialogsfcbe[W(&T)Replacement &Text miscdialogsOuYvS(&n)Reserve &numbers miscdialogsh9p Root Node miscdialogsd}"[W{&N2 {0} g*b~R0Search string "{0}" not found miscdialogsd}"eg, {0} g*b~R0Search text "{0}" not found miscdialogs [vRe/(&b)Selected &branches miscdialogs b邂pvT ~p(&s)Selection's &siblings miscdialogs b邂pv[Pp(&n)Selection's childre&n miscdialogs\Vh Small Icons miscdialogsc^eTSort Direction miscdialogsc^el Sort Method miscdialogspc^ Sort Nodes miscdialogsbQhg Spell Check miscdialogs]QwhT}N(&b)Tool&bar Commands miscdialogs]Qwh\:[(&S) Toolbar &Size miscdialogs ]QwhepToolbar Quantity miscdialogs]QwTools miscdialogs]QwSU Tools Menu miscdialogs hV[WOSTree View Font miscdialogs TreeLine - N%͕TreeLine - Serious Error miscdialogsTreeLineSTreeLine Numbering miscdialogsQe {0} v[x:Type Password for "{0}": miscdialogs Qe[x:Type Password: miscdialogs fepSUpdate Node Numbering miscdialogsOu(eNR[(&e)Use file &encryption miscdialogsVView miscdialogsVSU View Menu miscdialogsd}"Q[What to Search miscdialogsc^Q[ What to Sort miscdialogs fevQ[What to Update miscdialogszSWindow miscdialogszSSU Window Menu miscdialogsN QAOu(^[x'Zero-length passwords are not permitted miscdialogs [b@g [Wk] [All Fields] miscdialogs [b@g |{W] [All Types] miscdialogsT [WName nodeformatW( h`P\eom;epcnVh$Activate data editors on mouse hoveroptiondefaultsTHs AppearanceoptiondefaultsRO[X Auto SaveoptiondefaultsRbS_N k!Ou(veN!Automatically open last file usedoptiondefaults[P)POy ([WOS^SUOM)+Child indent offset (in font height units) optiondefaultsSUQpT}T Click node to renameoptiondefaultsepcnVhh<_Data Editor FormatsoptiondefaultsegDatesoptiondefaultsSu(RFeatures Availableoptiondefaults {,NY) kThFirst day of weekoptiondefaultsfgNFridayoptiondefaults.)(ObSSp)TreeLine JSONeN)Indent (pretty print) TreeLine JSON filesoptiondefaultsg\S^u(z ^R0|~bXv#Minimize application to system trayoptiondefaults$O[XNKvRep (N:0hy:yu()+Minutes between saves (set to 0 to disable)optiondefaultsfgNMondayoptiondefaultsgeNvep W(eNSUN-(Number of recent files in the file menuoptiondefaults dm~R+epNumber of undo levelsoptiondefaultsW(ezSN-bS_eNOpen files in new windowsoptiondefaults gveN Recent FilesoptiondefaultsR delՋvgeNgav'Remove inaccessible recent file entriesoptiondefaultsR^eT}T epRename new nodes when createdoptiondefaults`bY N NN*zSQOU Restore previous window geometryoptiondefaults`bY geNvhVr`(Restore tree view states of recent filesoptiondefaultsfgQmSaturdayoptiondefaultsf>y:bS\QyVQHVShow breadcrumb ancestor viewoptiondefaultsW(SbKVN-f>y:[Pzh<#Show child pane in right hand viewsoptiondefaultsW(QVN-f>y:TNShow descendants in output viewoptiondefaultsW(hVN-f>y:VhShow icons in the tree viewoptiondefaultsW(epcnVN-f>y:ep[f[Wk&Show math fields in the Data Edit viewoptiondefaultsW(epcnVN-f>y:S[Wk+Show numbering fields in the Data Edit viewoptiondefaultsT/RgaNStartup ConditionoptiondefaultsfgeSundayoptiondefaultsfgVThursdayoptiondefaultseTimesoptiondefaults hbe>Su(Tree drag && drop availableoptiondefaultsfgNTuesdayoptiondefaultsdmQ[X Undo MemoryoptiondefaultsfgN  Wednesdayoptiondefaults Sm(&C)&Canceloptions xn(&O)&OKoptions bMneNOMn"Choose configuration file locationoptionsz ^v_U(Od:_Ou()$Program directory (for portable use)optionsu(b7vN;v_U(cP)#User's home directory (recommended)optionsRYSbSSpg:eQError initializing printer printdata TreeLine - [QPDFTreeLine - Export PDF printdata,fTJ_SRMbSSpg:N e/c݋n0 O[XetJWarning: Margin settings unsupported on current printer. Save adjustments? printdata:fTJ_SRMbSSpg:N e/cubY'\T݋n0 O[Xubet]Warning: Page size and margin settings unsupported on current printer. Save page adjustments? printdata0fTJ_SRMbSSpg:N e/cubY'\n0 O[XetKWarning: Page size setting unsupported on current printer. Save adjustment? printdata N (&B):&Bottom: printdialogs Sm(&C)&Cancel printdialogs~[Ppu;~(&D)&Draw lines to children printdialogsethh(&E) &Entire tree printdialogs [WOS(&F)&Font printdialogs[WOS b(&F)&Font Selection printdialogs^8Đ y(&G)&General Options printdialogsuw ](&H) &Header Left printdialogsuw /u(&H)&Header/Footer printdialogsSbh9p(&I)&Include root node printdialogs"Oc{,NN*[PpW(r6pN (&K)&Keep first child with parent printdialogs ](&L):&Left: printdialogs Rep(&N)&Number of columns printdialogs xn(&O)&OK printdialogs RM(&P)&Prefix printdialogsbSSp(&P)... &Print... printdialogs S(&R):&Right: printdialogs T(&S)&Suffix printdialogs N (&T):&Top: printdialogs SUOM(&U)&Units printdialogs(Ou(TreeLineQV[WOS(&U)&Use TreeLine output view font printdialogs [(&W):&Width: printdialogs"A3 (279 x 420 mm)A3 (279 x 420 mm) printdialogs"A4 (210 x 297 mm)A4 (210 x 297 mm) printdialogs"A5 (148 x 210 mm)A5 (148 x 210 mm) printdialogs>AaBbCcDdEeFfGg...TtUuVvWvXxYyZzAaBbCcDdEeFfGg...TtUuVvWvXxYyZz printdialogsS|s (cm)Centimeters (cm) printdialogsRColumns printdialogs [NIY'\ Custom Size printdialogs؋[WOS Default Font printdialogsubY'\beeH(Error: Page size or margins are invalid printdialogsYe[W Extra Text printdialogsb[ub Facing Pages printdialogsry_Features printdialogs [Wk(&d)Fiel&ds printdialogs[Wkh<_(&m) Field For&mat printdialogs"{0}"v[Wkh<_Field Format for "{0}" printdialogsTubFit Page printdialogsT[^ Fit Width printdialogs[WOSh7_(&y) Font st&yle printdialogsu(&e):Foot&er: printdialogsu](&L) Footer &Left printdialogsuN-(&n)Footer Ce&nter printdialogsuS(&t) Footer Righ&t printdialogsh<_^.R(&H) Format &Help printdialogsuw (&a):He&ader: printdialogsuw S(&R) Header &Right printdialogsuw N-(&e)Header C&enter printdialogs uw TuHeader and Footer printdialogs:Height: printdialogs [(in) Inches (in) printdialogs SbvpIncluded Nodes printdialogs)Indent printdialogs )POy(&t) (~SUOM)"Indent Offse&t (line height units) printdialogs l4^s(&d) Lan&dscape printdialogs&l_eNf (8.5 x 14 in.)Legal (8.5 x 14 in.) printdialogs"O~ (8.5 x 11 in.)Letter (8.5 x 11 in.) printdialogsMargins printdialogsk|s (mm)Millimeters (mm) printdialogsTNu Next Page printdialogsSg bS_pv[Pp(&y)Onl&y open node children printdialogseT Orientation printdialogsQh<_(&F)Output &Format printdialogsubn(&S) Page &Setup printdialogs~_ \:[(&S) Paper &Size printdialogs zT(&i) Portra&it printdialogsRMNu Previous Page printdialogsbSSpPrint printdialogsbSSp(&v)...Print Pre&view... printdialogsbSSp Print Preview printdialogsbSSpn Print Setup printdialogsbSSpnPrinting Setup printdialogsy:OSample printdialogs bbSSpg:(&P)Select &Printer printdialogs b[WOS Select Font printdialogs [vRe/(&b)Selected &branches printdialogs [vp(&n)Selected &nodes printdialogs \:[(&z)Si&ze printdialogsSUu Single Page printdialogsRNKzzh<(&m)Space between colu&mns printdialogs$\Wb~ (11 x 17 in.)Tabloid (11 x 17 in.) printdialogs TreeLine PDF bSSpg:TreeLine PDF Printer printdialogsbSSpNNH What to print printdialogse>Y'Zoom In printdialogs)\Zoom Out printdialogs mR(&A)&Add spellcheck Sm(&C)&Cancel spellcheck_ueQh(&I) &Ignore All spellcheck fcb(&R)&Replace spellcheckmR\Q(&L)Add &Lowercase spellcheckN N e:Context: spellcheckVb~N R0aspell.exe ispell.exebhunspell.exe c[OMnQCould not find either aspell.exe, ispell.exe or hunspell.exe Browse for location? spellcheck[bhgRe/ NΘv~~3Finished checking the branch Continue from the top? spellcheck [bbQhgFinished spell checking spellcheck _ue(&e)Ignor&e spellcheckH[OMaspell.exe ipsell.exebhunspell.exe-Locate aspell.exe, ipsell.exe or hunspell.exe spellcheck N W([WQxN-Not in Dictionary spellcheckz ^(*.exe)Program (*.exe) spellcheckfcbQh(&p) Re&place All spellcheckbQhg Spell Check spellcheck bQhgSpell Check Error spellcheck^ Suggestions spellcheckTreeLinebQhgTreeLine Spell Check spellcheckTTreeLinebQhg xnO[Naspell ispellbhunspellLTreeLine Spell Check Error Make sure aspell, ispell or hunspell is installed spellcheck:Word: spellcheck W(hN- bSelect in Tree titlelistviewyv{&SBullets treeformats[P|{W ChildType treeformats [P|{WPR6ChildTypeLimit treeformatsgaNRConditionalRule treeformats؋DEFAULT treeformats O0HtmlEvalHtml treeformats[WkFIELD treeformatseNFILE treeformats[Wk|{W FieldType treeformatsh<_Format treeformats h<_Html FormatHtml treeformatsN,|{W GenericType treeformatsVhIcon treeformatsRYP< InitialValue treeformats RhRRr{& ListSeparator treeformatsep[W~NumLines treeformatsQh<_ OutputFormat treeformatsRMPrefix treeformatsTRMc^ SortForward treeformats c^.S SortKeyNum treeformatsN-zzh< SpaceBetween treeformatsTSuffix treeformats|{WTYPE treeformatshh<Table treeformatshh<_ TitleFormat treeformats |OS(&B) &Bold Fonttreelocalcontrol b(&C)&CopytreelocalcontrolR dp(&D) &Delete NodetreelocalcontrolRyQK(&D)&Detach Clonestreelocalcontrol[Q(&E)... &Export...treelocalcontrolYc(&E)...&External Link...treelocalcontrol[WOSY'\(&F) &Font Sizetreelocalcontrol)ۂp(&I) &Indent Nodetreelocalcontrol eOS(&I) &Italic Fonttreelocalcontrol N y(&M)&Move Uptreelocalcontrole^zS(&N) &New Windowtreelocalcontrol |4(&P)&PastetreelocalcontrolbSSp(&P)... &Print...treelocalcontrol PZ(&R)&RedotreelocalcontroleubS€(&R)&Regenerate ReferencestreelocalcontrolT}T (&R)&Renametreelocalcontrol O[X(&S)&Savetreelocalcontrolnp|{W(&S)&Set Node TypetreelocalcontrolbQhg(&S)...&Spell Check...treelocalcontrol dm(&U)&UndotreelocalcontrolSm)ۂp(&U)&Unindent NodetreelocalcontrolmR[Pp(&C) Add &ChildtreelocalcontrolmR|{R+~R+(&L)...Add Category &Level...treelocalcontrol\e[PymRR0 [vr6y Add new child to selected parenttreelocalcontrolmRbOe9YWebc!Add or modify an extrnal web linktreelocalcontrolmRbOe9Q肂pc#Add or modify an internal node linktreelocalcontrollg ^8[Wkelbi\U#Cannot expand without common fieldstreelocalcontrol|{R+[WkCategory Fieldstreelocalcontrolndh<_(&m)Clear For&mattingtreelocalcontrolnd_SRMb [veg,h<_)Clear current or selected text formattingtreelocalcontrolQKb@g S9Mvp(&M)Clone All &Matched NodestreelocalcontrolT^v[WkbSTN&Collapse descendants by merging fieldstreelocalcontrol\b@g S9MvplcbN:QK&Convert all matching nodes into clonestreelocalcontrol\{0}Re/lcbN:QK"Converted {0} branches into clonestreelocalcontrolNeNY R6|{W(&F)...Copy Types from &File...treelocalcontrol\Re/beg,Y R6R0Rj4g(Copy the branch or text to the clipboardtreelocalcontrol$NSNN*TreeLineeNY R6Mn1Copy the configuration from another TreeLine filetreelocalcontrol RjR(&t)Cu&ttreelocalcontrol\Re/beg,RjRR0Rj4g'Cut the branch or text to the clipboardtreelocalcontrol؋Defaulttreelocalcontrol R db@ pDelete the selected nodestreelocalcontrolRy_SRMRe/N-vb@g QKp+Detach all cloned nodes in current branchestreelocalcontrol - elR dYNeN{}'Error - could not delete backup file {}treelocalcontrol - elՋSeN{0}Error - could not read file {0}treelocalcontrol - elQQeeNError - could not write to filetreelocalcontrol - elQQe{}Error - could not write to {}treelocalcontrolNTyQvNh<_[QeN(Export the file in various other formatstreelocalcontrolOu(_SRMbSSp y[QN:PDF+Export to PDF with current printing optionstreelocalcontrol eN]O[X File savedtreelocalcontrolc |{R+bT(&b)Flatten &by Categorytreelocalcontrol[WOSr(&o)...Font C&olor...treelocalcontrol_:R6feb@g gaN|{WTep[f[Wk3Force update of all conditional types & math fieldstreelocalcontrol)ې [vpIndent the selected nodestreelocalcontrolNKTcQeT ~(&A)Insert Sibling &AftertreelocalcontrolNKRMcQeT ~(&B)Insert Sibling &BeforetreelocalcontrolW([PyN ecQe|{R+p$Insert category nodes above childrentreelocalcontrolW( b邂pNKTcQeevT ~p"Insert new sibling after selectiontreelocalcontrolW( b邂pNKRMcQeevT ~p#Insert new sibling before selectiontreelocalcontrolQc(&L)...Internal &Link...treelocalcontrolY'LargetreelocalcontrolfY'LargertreelocalcontrolgY'Largesttreelocalcontrol N y(&o) M&ove DowntreelocalcontrolyR0OM(&F) Move &FirsttreelocalcontrolyR0gT(&L) Move &LasttreelocalcontrolN y [vpMove the selected nodes downtreelocalcontrol\b@ pyRN:{,NN*[Pp0Move the selected nodes to be the first childrentreelocalcontrol\b@ pyRN:gTNN*[Pp/Move the selected nodes to be the last childrentreelocalcontrolN y [vpMove the selected nodes uptreelocalcontrolb~N R0vT vpNo identical nodes foundtreelocalcontrolbS_T NeNvezS#Open a new window for the same filetreelocalcontrolbSSpn(&r)...P&rint Setup...treelocalcontrol|4~eg,(&s)Pa&ste Plain Texttreelocalcontrol|4[Pp(&h) Paste C&hildtreelocalcontrol|4QKv[Pp(&o)Paste Cl&oned ChildtreelocalcontrolNKRM|4QKvT ~(&n)Paste Clo&ned Sibling BeforetreelocalcontrolNKT|4QKvT ~(&d)Paste Clone&d Sibling AftertreelocalcontrolNKT|4T ~(&A)Paste Sibling &AftertreelocalcontrolNKRM|4T ~(&B)Paste Sibling &BeforetreelocalcontrolNRj4g|4QKv[Pp&Paste a child clone from the clipboardtreelocalcontrolNRj4g|4[Pp%Paste a child node from the clipboardtreelocalcontrolW( b邂pNKT|4evT ~pPaste a sibling after selectiontreelocalcontrolW( b邂pNKRM|4evT ~p Paste a sibling before selectiontreelocalcontrol"W( b邂pNKT|4evT ~QKp%Paste a sibling clone after selectiontreelocalcontrol"W( b邂pNKRM|4evT ~QKp&Paste a sibling clone before selectiontreelocalcontrol|4Rj4gN-vpbeg,&Paste nodes or text from the clipboardtreelocalcontrolNRj4g|4g*h<_Sveg,+Paste non-formatted text from the clipboardtreelocalcontrolbSSpR0PDF(&t)...Print &to PDF...treelocalcontrolbSSp(&v)...Print Pre&view...treelocalcontrolh9cn_SRM ybSSphQ*Print tree output based on current optionstreelocalcontrol\^`'(&e)...Prop&erties...treelocalcontrolPZN NN*dmRedo the previous undotreelocalcontrolT}T _SRMhgavh#Rename the current tree entry titletreelocalcontrolNcb|{R+~R+(&w)S&wap Category LevelstreelocalcontrolS[XN:(&A)... Save &As...treelocalcontrolO[XeN Save Filetreelocalcontrol\fe9O[XR0{}Save changes to {}?treelocalcontrol O[Xfe9 Save changes?treelocalcontrol O[X_SRMeNSave the current filetreelocalcontrolOu(eT yO[XeNSave the file with a new nametreelocalcontrol be~R+v[WkSelect fields for new leveltreelocalcontrol n[WOSY'\ Set Font Sizetreelocalcontrol np|{W Set Node TypetreelocalcontrolnS)TR[{IeNSep3Set file parameters like compression and encryptiontreelocalcontrol n ubY'\TQvNbSSp y1Set margins, page size and other printing optionstreelocalcontroln_SRMbb@ eg,vY'\(Set size of the current or selected texttreelocalcontroln_SRMbb@ eg,vr-Set the color of the current or selected texttreelocalcontrol\_SRMb [v[WOSnN:|OS(Set the current or selected font to boldtreelocalcontrol\_SRMb [v[WOSnN:eOS*Set the current or selected font to italictreelocalcontrol\_SRMb [v[WOSnN:N R~-Set the current or selected font to underlinetreelocalcontrolnb@ pvp|{W$Set the node type for selected nodestreelocalcontrolf>y:bSSp~gv"Show a preview of printing resultstreelocalcontrol\SmalltreelocalcontrolbQhghveg,epcn Spell check the tree's text datatreelocalcontrolNcb[PpT[Yp|{p(Swap child and grandchild category nodestreelocalcontrol$TreeLine - bS_MneN"TreeLine - Open Configuration FiletreelocalcontrolTreeLine - S[XN:TreeLine - Save AstreelocalcontrolN R~(&n)U&nderline FonttreelocalcontroldmN NN*dO\Undo the previous actiontreelocalcontrolSm)ې [vpUnindent the selected nodestreelocalcontrol2fTJ - eNc_WO W(NN pN-NWO[P_u(OWarning - file corruption! Skipped bad child references in the following nodes:treelocalcontrol"QsNTreeLine(&A)...&About TreeLine...treemaincontrolWg,u(l(&B)...&Basic Usage...treemaincontrolSmeNbS_(&C)&Cancel File OpentreemaincontrolgaNgb~(&C)...&Conditional Find...treemaincontrolMnepcn|{W(&C)...&Configure Data Types...treemaincontrolR dYN(&D)&Delete Backuptreemaincontrolgb~eg,(&F)... &Find Text...treemaincontrol[etehc(&F)...&Full Documentation...treemaincontrol^8Đ y(&G)...&General Options...treemaincontrol[Qe(&I)... &Import...treemaincontrole^(&N)...&New...treemaincontrolbS_(&O)...&Open...treemaincontrol Q(&Q)&Quittreemaincontrol`bY YN(&R)&Restore Backuptreemaincontrol bb@g (&S) &Select Alltreemaincontrol by:OeN(&S)&Select Sampletreemaincontrol bj!g(&S)&Select Templatetreemaincontroleg,nVh(&T)...&Text Filter...treemaincontrol.YNeN {} [XW(0 NKRMvOS]~])ny:Wg,Ou(f Display basic usage instructionstreemaincontrolf>y:g Qskdz ^vrHg,O`o'Display version info about this programtreemaincontrol - b~N R0Wg,^.ReN!Error - basic help file not foundtreemaincontrol - elՋSeN{0}Error - could not read file {0}treemaincontrol - elR dYNeN{}'Error - could not remove backup file {}treemaincontrol, - el\ {0} T}T N: {1} 'Error - could not rename "{0}" to "{1}"treemaincontrol  - el\MneNQQe{})Error - could not write config file to {}treemaincontrol - b~N R0ehceN$Error - documentation file not foundtreemaincontrol* - eeHvTreeLineeN{0}!Error - invalid TreeLine file {0}treemaincontrolQ^u(Exit the applicationtreemaincontroln䂂pNf>y:S9Meg,&Filter nodes to only show text matchestreemaincontrolgb~Tfcb(&R)...Find and &Replace...treemaincontrolW(hTepcnN-gb~eg,Find text in node titles & datatreemaincontrol^8Đ yGeneral Optionstreemaincontrol^rHg,:Library versions:treemaincontrolepcn|{W [WkTQL(Modify data types, fields & output linestreemaincontroleeNNew FiletreemaincontrolbS_eN Open FiletreemaincontrolbS_y:OeN(&m)...Open Sa&mple...treemaincontrol bS_y:OeN Open SampletreemaincontrolbS_NN*y:OeNOpen Sample Filetreemaincontrol$Ou([etehcbS_TreeLineeN,Open a TreeLine file with full documentationtreemaincontrolNxvN bS_eNOpen a file from disktreemaincontrolbS_NN*^TreeLineeNOpen a non-TreeLine filetreemaincontrolbS_NN*y:OeNOpen a sample filetreemaincontrolfcbpepcnN-veg,[W{&N2!Replace text strings in node datatreemaincontrolW(Vh̐ bb@g Select all text in an editortreemaincontroln.v_cw.(&K)...Set &Keyboard Shortcuts...treemaincontrolnb@g eNvu(b7 y"Set user preferences for all filestreemaincontrolf>y:Mn~g(&o)... Show C&onfiguration Structure...treemaincontrolf>y:Sv|{W~gSS.Show read-only visualization of type structuretreemaincontrolpc^(&t)...Sor&t Nodes...treemaincontrol_YNN*eeNStart a new filetreemaincontrolTreeLine - bS_eNTreeLine - Open FiletreemaincontrolTreeLineWg,u(lTreeLine Basic UsagetreemaincontrolTreeLinerHg,{0}TreeLine version {0}treemaincontrolfeS(&N)...Update &Numbering...treemaincontrolfepepPy:[Pzh<(&S)&Show Child Pane treewindow ]Qw(&T)&Tools treewindow V(&V)&View treewindow zS(&W)&Window treewindow QsN*zSClose this window treewindowbS [pvb@g [Pp+Collapse all children of the selected nodes treewindowepcn Data Edit treewindowepcnQ Data Output treewindow\U_ [pvb@g [Pp)Expand all children of the selected nodes treewindow h<_(&r)Fo&rmat treewindowSR0N NN*SSh b(Go to the next tree selection in history treewindowN NN*Xd}"Next Incremental Search treewindowN NN*Xd}"Previous Incremental Search treewindowVN NN*h b%Return to the previous tree selection treewindowf>y:bS\QV(&B)Show &Breadcrumb View treewindowf>y:hRh(&T)Show &Title List treewindowf>y:epcnVh(&E)Show Data &Editor treewindowf>y:epcnQ(&O)Show Data &Output treewindowf>y:QTN(&D)Show Output &Descendants treewindowW(SOVf>y:epcnVhShow data editor in right view treewindowW(SOVf>y:epcnQShow data output in right view treewindowW(SOVf>y:hRhShow title list in right view treewindow _YXd}"Start Incremental Search treewindowhRh Title List treewindowRcbf>y:bS\QyVV'Toggle showing breadcrumb ancestor view treewindowRcbf>y:QV)TN/Toggle showing output view indented descendants treewindowRcbf>y:SO[PV%Toggle showing right-hand child views treewindowTreeLine/translations/qt_de.qm0000644000175000017500000006560313353736652015446 0ustar dougdougʴ5>4 Ac>} K!?> bb?( b`? la@ la@8 lf@n t@ @ A ˰BU B %'C- C )D */D9 =Dr BD T^EU c(E eE JE %pF ,Fa F ˔F :G f GO sG G 0NH E9H Mc\I* f)Ir I 5TKa HK $K .@L iLO L L JMF ̺M -DM kN 0NZ N .N RVOC RVO| SO YO [P j7oP pQ\ Q R R %S( Si +>S ;ɾS PtT ^+dTK feT gT iFCU iU uU wV1 w}V ^V RW t5W WpSX XRuXGa.XYY>ݖY[yZ  Z`%4Z-vZ0i)[[0[2wT[a\c5\{~a\`]5]N]ky^J_0P_t2_i`J**QWidget++ QShortcutAMAM QDateTimeEditOKOK QColorDialogOKOKQDialogButtonBoxOKOK QMessageBoxOKOK QPrintDialogOKOKQPrintPropertiesDialogPMPM QDateTimeEditNeinNo QShortcutHochUp QShortcutamam QDateTimeEditpmpm QDateTimeEditto QPrintDialog&OK&OK QErrorMessage N&ein&NoQDialogButtonBoxAltAlt QShortcutF%1F%1 QShortcutEntfDel QShortcutEndeEnd QShortcutEscEsc QShortcut EinfgIns QShortcutTabTab QShortcut AnfangTop QScrollBarXIMXIM QInputContextJaYes QShortcutFehler: Fatal Error: QErrorMessage&Ja&YesQDialogButtonBox ZurckBack QFileDialog ZurckBack QShortcut AnrufCall QShortcut&AusschneidenCu&t QLineEdit&AusschneidenCu&t QTextControlStrgCtrl QShortcut RunterDown QShortcutBeendenExitQMenuBar DateiFile QFileDialog DateiFile QPrintDialogUmdrehenFlip QShortcut HilfeHelpQDialogButtonBox HilfeHelp QMessageBox HilfeHelp QShortcutPos1HomeQObjectPos1Home QShortcutKind QDirModel LinksLeft QShortcutMenMenu QShortcutMetaMeta QShortcutNameNamePPDOptionsModelNameName QDirModel ffnenOpenQDialogButtonBox ffnenOpen QFileDialogBild aufwrtsPgUp QShortcut WiederherstellenRedo QUndoGroup WiederherstellenRedo QUndoStackBeendenQuitQMenuBarSpeichernSaveQDialogButtonBoxSpeichernSaveQPrintPropertiesDialog GreSize QDirModelSortierenSort QFileDialogAbbrechenStop QShortcutWahrTrueQObjectRckgngigUndo QUndoGroupRckgngigUndo QUndoStack"A6 (105 x 148 mm)A6 (105 x 148 mm) QPrintDialog&Kopieren&Copy QLineEdit&Kopieren&Copy QTextControl&Schriftart&Font QFontDialogVer&schieben&Move QWorkspace&ffnen&Open QFileDialog &Rot:&Red: QColorDialog"Wieder&herstellen&Redo QLineEdit"Wieder&herstellen&Redo QTextControl &Sat:&Sat: QColorDialogS&peichern&Save QFileDialog &Gre&Size QFontDialog&Gre ndern&Size QWorkspace&Rckgngig&Undo QLineEdit&Rckgngig&Undo QTextControl &Val:&Val: QColorDialogFLegal (8,5 x 14 Zoll, 216 x 356 mm)%Legal (8.5 x 14 inches, 216 x 356 mm) QPrintDialogAbbrechenAbortQDialogButtonBoxberAboutQMenuBarAnwendenApplyQDialogButtonBoxSchlieenCloseQDialogButtonBoxSchlieenClose QWorkspaceLaufwerkDrive QFileDialog EnterEnter QShortcut FalschFalseQObjectFarb&ton:Hu&e: QColorDialogMinimierenMinimize QWorkspaceAlles drucken Print all QPrintDialog ffnenOpen  QFileDialog PausePause QShortcutSonstigesOther QPrintDialog DruckPrint QPrintDialog DruckPrint QShortcutResetQDialogButtonBoxWiederholenRetryQDialogButtonBox RechtsRight QShortcutEinrichtenSetupQMenuBarUmschaltShift QShortcutSize: QPrintDialogLeertasteSpace QShortcutValuePPDOptionsModelDirekthilfe What's This?QDialogDirekthilfe What's This?QWhatsThisAction

About Qt

%1

Qt is a C++ toolkit for cross-platform application development.

Qt provides single-source portability across MS Windows, Mac OS X, Linux, and all major commercial Unix variants. Qt is also available for embedded devices as Qtopia Core.

Qt is a Trolltech product. See www.trolltech.com/qt/ for more information.

 QMessageBoxRestore DefaultsQDialogButtonBoxAufzeichnen Media Record QShortcut"B6 (125 x 176 mm)B6 (125 x 176 mm) QPrintDialog$Bildschirm drucken Print Screen QShortcut8Eigene Farben &definieren >>&Define Custom Colors >> QColorDialogPapierquelle: Paper source: QPrintDialogB8 (62 x 88 mm)B8 (62 x 88 mm) QPrintDialogA8 (52 x 74 mm)A8 (52 x 74 mm) QPrintDialogB9 (44 x 62 mm)B9 (44 x 62 mm) QPrintDialogA9 (37 x 52 mm)A9 (37 x 52 mm) QPrintDialog*Nach &Gre sortieren Sort by &Size QFileDialog&B0 (1000 x 1414 mm)B0 (1000 x 1414 mm) QPrintDialog*Nach &Datum sortieren Sort by &Date QFileDialog(Nach &Name sortieren Sort by &Name QFileDialog9'%1' is write protected. Do you want to delete it anyway? QFileDialogDAktiviert das Programmhauptfenster#Activates the program's main window QApplicationShow Details... QMessageBox"A5 (148 x 210 mm)A5 (148 x 210 mm) QPrintDialog,Tabloid (279 x 432 mm)Tabloid (279 x 432 mm) QPrintDialogHRLE Start of right-to-left embedding$RLE Start of right-to-left embeddingQUnicodeControlCharacterMenu.Im &Vordergrund bleiben Stay on &Top QWorkspaceJDiese Meldungen noch einmal an&zeigen&Show this message again QErrorMessage B10 (31 x 44 mm)B10 (31 x 44 mm) QPrintDialog,Windows-EingabemethodeWindows input method QInputContext&Dekrementieren Step &downQAbstractSpinBoxHhen - Treble Down QShortcut$XIM-EingabemethodeXIM input method QInputContext"B2 (500 x 707 mm)B2 (500 x 707 mm) QPrintDialogSchl&ieen&Close QWorkspacePPD PropertiesQPrintPropertiesDialogEinf&gen&Paste QLineEditEinf&gen&Paste QTextControl Selection QPrintDialog*Rollen-Feststelltaste Scroll Lock QShortcut&Nach unten scrollen Scroll down QScrollBarHier scrollen Scroll here QScrollBar&Nach links scrollen Scroll left QScrollBar QUndoModel"A3 (297 x 420 mm)A3 (297 x 420 mm) QPrintDialog*Die Selektion DruckenPrint selection QPrintDialog%1 - [%2] %1 - [%2] QWorkspace*ZWSP Zero width spaceZWSP Zero width spaceQUnicodeControlCharacterMenuLautstrke - Volume Down QShortcutTon aus Volume Mute QShortcut"B4 (250 x 353 mm)B4 (250 x 353 mm) QPrintDialogVorherigerMedia Previous QShortcut Home Page QShortcutPapierformat Paper format QPrintDialogLautstrke + Volume Up QShortcut Print dialog QPrintDialog/%1 already exists. Do you want to overwrite it? QPrintDialog"A1 (594 x 841 mm)A1 (594 x 841 mm) QPrintDialogFarbauswahl Select color QColorDialog Printer info: QPrintDialogRZu benutzerdefinierten Farben &hinzufgen&Add to Custom Colors QColorDialog Bla&u:Bl&ue: QColorDialogLTRQT_LAYOUT_DIRECTION QApplicationEndeBottom QScrollBarAbbrechenCancel QColorDialogAbbrechenCancelQDialogButtonBoxAbbrechenCancel QPrintDialogAbbrechenCancelQProgressDialogBrowse QPrintDialogStart (6) Launch (6) QShortcutStart (7) Launch (7) QShortcutStart (8) Launch (8) QShortcutStart (9) Launch (9) QShortcutStart (2) Launch (2) QShortcutStart (3) Launch (3) QShortcutStart (4) Launch (4) QShortcutStart (5) Launch (5) QShortcutStart (0) Launch (0) QShortcutStart (1) Launch (1) QShortcutStart (F) Launch (F) QShortcutStart (B) Launch (B) QShortcutStart (C) Launch (C) QShortcutStart (D) Launch (D) QShortcutStart (E) Launch (E) QShortcutStart (A) Launch (A) QShortcutKonfigurationConfigQMenuBarCopies QPrintDialogLschenDelete QLineEditLschenDelete QShortcutLschenDelete QTextControlQuerformat Landscape QPrintDialog EscapeEscape QShortcutAuflegenHangup QShortcut"ElternverzeichnisParent Directory QFileDialog$B1 (707 x 1000 mm)B1 (707 x 1000 mm) QPrintDialog> File not found. Please verify the correct file name was given QFileDialogIgnorierenIgnoreQDialogButtonBoxEinfgenInsert QShortcut(Folio (210 x 330 mm)Folio (210 x 330 mm) QPrintDialogBass Boost Bass Boost QShortcutBild abwrtsPgDown QShortcut$DLE (110 x 220 mm)DLE (110 x 220 mm) QPrintDialog ReturnReturn QShortcutBeispielSample QFontDialog&AufrollenSh&ade QWorkspace SuchenSearch QShortcutAuswhlenSelect QShortcut&Unsortiert &Unsorted QFileDialog$C5E (163 x 229 mm)C5E (163 x 229 mm) QPrintDialog"B5 (176 x 250 mm)%B5 (176 x 250 mm, 6.93 x 9.84 inches) QPrintDialog SysReqSysReq QShortcut*Zahlen-FeststelltasteNumLock QShortcutMein Computer My Computer QFileDialogBereich drucken Print range QPrintDialog*Rollen-Feststelltaste ScrollLock QShortcutClose without SavingQDialogButtonBoxKontext1Context1 QShortcutKontext2Context2 QShortcutKontext3Context3 QShortcutKontext4Context4 QShortcutSuchen in:Look in: QFileDialog$Anzahl der Kopien:Number of copies: QPrintDialogOptionenOptionsQMenuBarBild aufwrtsPage Up QShortcut(Eine Seite nach obenPage up QScrollBar File exists QPrintDialog.Mac OS X-EingabemethodeMac OS X input method QInputContextHochformatPortrait QPrintDialogffne URLOpen URL QShortcut Properties QPrintDialogDebug Ausgabe:Debug Message: QErrorMessageFeststelltaste Caps Lock QShortcutDateiname: File name: QFileDialogSeitengre: Page size: QPrintDialog,Eine Seite nach rechts Page right QScrollBarDruckerPrinter QPrintDialogN&ein zu allem N&o to AllQDialogButtonBoxDouble side printing QPrintDialog$A0 (841 x 1189 mm)A0 (841 x 1189 mm) QPrintDialogSchrifts&til Font st&yle QFontDialog8Farbig drucken falls mglichPrint in color if available QPrintDialog direkt verbundenlocally connected QPrintDialog*Ledger (432 x 279 mm)Ledger (432 x 279 mm) QPrintDialog"Dateien des Typs:Files of type: QFileDialogSystem RequestSystem Request QShortcutFeststelltasteCapsLock QShortcutRck-TabBacktab QShortcut Bass +Bass Up QShortcutWarnung:Warning: QErrorMessage&SchriftsystemWr&iting System QFontDialogAktualisierenRefresh QShortcutQuit %1QMenuBarSave AllQDialogButtonBox"Wieder&herstellen&Restore QWorkspace8&Versteckte Dateien anzeigenShow &hidden files QFileDialog!Are sure you want to delete '%1'? QFileDialog@Unicode-Kontrollzeichen einfgen Insert Unicode control characterQUnicodeControlCharacterMenu$Nach oben scrollen Scroll up QScrollBarber QtAbout QtQMenuBarber QtAbout Qt QMessageBoxAbout %1QMenuBarSpeichern unterSave As QFileDialogAlias: %1 Aliases: %1 QPrintDialog%1 Das Verzeichnis konnte nicht gefunden werden. Stellen Sie sicher, dass der Verzeichnisname richtig ist.K%1 Directory not found. Please verify the correct directory name was given. QFileDialogEinstellungenSettingQMenuBarFLRO Start of left-to-right override#LRO Start of left-to-right overrideQUnicodeControlCharacterMenuHLRE Start of left-to-right embedding$LRE Start of left-to-right embeddingQUnicodeControlCharacterMenuStart Mail Launch Mail QShortcutPrint To File ... QPrintDialogRcktaste Backspace QShortcut(Schriftart auswhlen Select Font QFontDialogJUS Common #10 Envelope (105 x 241 mm)%US Common #10 Envelope (105 x 241 mm) QPrintDialog Bass - Bass Down QShortcutCollate QPrintDialog Liste List View QFileDialogStandbyStandby QShortcut&Inkrementieren&Step upQAbstractSpinBox&Lschen&Delete QFileDialogAktivierenActivate QApplication&Unterstrichen &Underline QFontDialog &Grn:&Green: QColorDialog Alle Dateien (*) All Files (*) QFileDialogVerzeichnisse Directories QFileDialogDiscardQDialogButtonBoxjApplikation '%1' bentigt Qt %2, aber Qt %3 gefunden.,Executable '%1' requires Qt %2, found Qt %3. QApplication4Whlen Sie ein VerzeichnisSelect a Directory QFileDialogJa zu &allem Yes to &AllQDialogButtonBox$Alle Dateien (*.*)All Files (*.*) QFileDialog|

This program uses Qt Open Source Edition version %1.

Qt Open Source Edition is intended for the development of Open Source applications. You need a commercial Qt license for development of proprietary (closed source) applications.

Please see www.trolltech.com/company/model/ for an overview of Qt licensing.

 QMessageBoxErne&ut laden&Reload QFileDialog&Umbenennen&Rename QFileDialogCould not delete directory. QFileDialogHhen + Treble Up QShortcut"A4 (210 x 297 mm)%A4 (210 x 297 mm, 8.26 x 11.7 inches) QPrintDialogAlles auswhlen Select All QLineEditAlles auswhlen Select All QTextControlVerzeichnis: Directory: QFileDialogEffekteEffects QFontDialog Durch&gestrichen Stri&keout QFontDialog(Nach rechts scrollen Scroll right QScrollBar*Zahlen-FeststelltasteNum Lock QShortcut*Zahlen-Feststelltaste Number Lock QShortcutGrundfar&ben &Basic colors QColorDialogUnbekanntUnknown QFileDialogunbekanntunknown QPrintDialog Pages from QPrintDialog&Herabrollen&Unshade QWorkspace,RLM Right-to-left markRLM Right-to-left markQUnicodeControlCharacterMenu|Die Datei %1 existiert bereits. Soll sie berschreiben werden?-%1 already exists. Do you want to replace it? QFileDialog4&Benutzerdefinierte Farben&Custom colors QColorDialog%Do you want to overwrite it? QPrintDialogNExecutive (7,5 x 10 Zoll, 191 x 254 mm))Executive (7.5 x 10 inches, 191 x 254 mm) QPrintDialog<PDF Pop directional formattingPDF Pop directional formattingQUnicodeControlCharacterMenuEinstellungen PreferenceQMenuBar PreferencesQMenuBarA&lphakanal:A&lpha channel: QColorDialogCopy &Link Location QTextControlM&inimieren Mi&nimize QWorkspace6Letzte Seite zuerst druckenPrint last page first QPrintDialogLinke Seite Left edge QScrollBarHLetter (8,5 x 11 Zoll, 216 x 279 mm)&Letter (8.5 x 11 inches, 216 x 279 mm) QPrintDialogFavoriten Favorites QShortcutVorwrtsForward QShortcutBild abwrts Page Down QShortcut*Eine Seite nach unten Page down QScrollBar*Eine Seite nach links Page left QScrollBarHide Details... QMessageBox4ZWNJ Zero width non-joinerZWNJ Zero width non-joinerQUnicodeControlCharacterMenuMa&ximieren Ma&ximize QWorkspace Wiederherstellen Restore Down QWorkspaceTypAll other platformsType QDirModelFRLO Start of right-to-left override#RLO Start of right-to-left overrideQUnicodeControlCharacterMenu Don't SaveQDialogButtonBox Print to file QPrintDialog*ZWJ Zero width joinerZWJ Zero width joinerQUnicodeControlCharacterMenu=File %1 is not writable. Please choose a different file name. QPrintDialog"B3 (353 x 500 mm)B3 (353 x 500 mm) QPrintDialog,Neuen Ordner erstellenCreate New Folder QFileDialog Date Modified QDirModel,LRM Left-to-right markLRM Left-to-right markQUnicodeControlCharacterMenuWiedergabe Media Play QShortcut Stopp Media Stop QShortcutNchster Media Next QShortcut^<p>Dieses Programm verwendet Qt Version %1.</p>'

This program uses Qt version %1.

 QMessageBoxRechte Seite Right edge QScrollBar$Start Media Player Launch Media QShortcut A7 (74 x 105 mm)A7 (74 x 105 mm) QPrintDialogDetails Detail View QFileDialog%1 Die Datei konnte nicht gefunden werden. Stellen Sie sicher, dass der Dateiname richtig ist.A%1 File not found. Please verify the correct file name was given. QFileDialog"A2 (420 x 594 mm)A2 (420 x 594 mm) QPrintDialog"Druckausrichtung: Orientation: QPrintDialog B7 (88 x 125 mm)B7 (88 x 125 mm) QPrintDialog<Qt Bibliothek ist inkompatibelIncompatible Qt Library Error QApplicationTreeLine/translations/qt_es.qm0000644000175000017500000010245513353736645015464 0ustar dougdoug;`;;;;M OO-7}ImS](5c+;o+;+O+OH4HJKLDLPSZr+[`7\O_[_s1}-,CUx%%%%90M0m005 D+,Y RZ ^Zg d\]4 x\ |^ vv@`f߮IA[Iyɵnɵnɵnɵn3ɵnJɵn`ɵny B1 YMqH<po5#Q%UT*42C'CdCCeCeD"!D"^D1MaR?4fP:l^oRdw^|{yW2j. d0yi6ur< B"lH)-/=N1$5~ < d?NNkyT]|``) 6s^= &,fE{8AA [ynL ME*E_ww!e)*/e52;8ByOZf` Bcփ Hu( jl p v$ |$ !!(!^!ֈ7"n n"t,"z";y"""&H"/"IxS"YM#>YM#yh^#i#ssc$=w$C$I$ۊ$N$]$I$I$I%%I%KI%rI%I%I%I&n&*I&0Y&Ji&dy&~&&&&''I'4'N'h'''uD'uD( o(A,(G,(i,(,(,((ɘe(5$(fR)N)6)Nc)z*^*,Pq*2V*~*** **+&%C+,?"+LKN+RM+XR+]+]+k+y^+{y++G%+ǥ,,++,At,]t,{y,r,-(-:%-@C--F5-ƨ-ƨ-˾-ҝz-է?.f.6~b.to.!.+3./.6 /G/4LAU/~U/U/Z/Z/Z0Z0&]k*0>^n0Xe0i0i1rg19y;1a}u1g}w1}w122@t2zt2.2.2P2D3 t3t3t3_ 4%F4=ʢ4Cʢ4Xd4^d4sd4d4594֣44U4B5w5E 5K25Q65D5K6U|6/ar6_p6et6}wZ6}$6}$7Z7LK<7R7l7 7/7E8,@58du8T8i~99h5kE9XU 9bD9gA:i$:nx1 :tz*2:d:U:5:z:҉;m;n;8;\O~;vC;|ʴ5;Ԅ;D<d<F5="Y=(I=.As=b =z }$= qe= ڤ= E>. E>E Ac>K Ac> 35> K!?? bb? b`?l i3? la@ |@6 t@ t@ @ @ A A >A A A B KB B %'Bb B C> C^ )Cd */Cj 7uC| =C BC T^D2 ]DX `D `D c(D dD eD eE f1E& gEj gnE k,F rD"F( xFp ~Fv F| 9F IF IG! ;G9 Ge G| JG %pG ,G ,G G ˔G PH H+ Hd 68H :H f H f I5 4Ig sIm sI AAI J m,J #-tJX 0NJ E9K& E9Ko LK Mc\K SK VK ]$L/ f)L5 f)L| io>L m`M wM 5TM< HMT HM $M .@M M iN Na N N JN JO t.O9 kO? ӇOE OK ̺Ow -DO kO U)O <P 0P  P-  Pd P xHP .P 7FQ >QP >Q >Q >Q >R$ >RZ DTR IR RVR RVR S.S SSN YST [Sv j7oS pT .T< BTB TH TT TT TT TT T T T )dU0 U .U .U .U .V aV yVW V] Vc tV :bV ʜW+ DW1  rW7 +>W 0EW ;ɾX PtX ^+dX feY- gYQ iFCY iY uY wY wZ8 w}Zj w}Z Z [ ^[> R[D o[~ X[ D[ t5\ \( )\L \T\gT\pS\*\/E\I_]$XRu][ ]a.^PvɅ^Vy$^p~^^S^B_V__ݖ`[y`  `H `"#`$U`%4a-va 0i)a?0ak1ca2wTaDaHbqJdbL$.bacc5cyCd{~ad`ddeNe(kyeJePft2f.fvffif*+AMAceptar Q3TabDialogAceptar QAxSelectAceptar QColorDialogAceptarQDialogButtonBoxAceptar QMessageBoxAceptar QPrintDialogAceptarPMNo QShortcutNo Arribaampm%1%&Aceptar Q3FileDialog&Aceptar&No Q3FileDialog&NoAltF%1SuprDirFinEscInsTabPrincipioXIMS QShortcutSError fatal:&S Q3FileDialog&S,Precedente (histrico) Q3FileDialogAnterior QFileDialogAnterior LlamarCor&tar Q3TextEditCor&tar QLineEditCor&tar FechaCtrl AbajoFichero Q3FileDialogFichero QFileDialogFicheroVoltear Ayuda Q3TabDialog AyudaQDialogButtonBox Ayuda QMessageBox Ayuda InicioIzquierdaMenMeta NombrePPDOptionsModel Nombre Q3FileDialog Nombre Abrir Q3FileDialog Abrir QComboBox AbrirQDialogButtonBox Abrir QFileDialog AbrirQMenu Abrir QPushButton Abrir Re Pg QUndoGroupGuardarQDialogButtonBoxGuardar QFileDialogGuardar Tamao Q3FileDialog TamaoOrdenar Q3FileDialogOrdenarDetenerVerdadero Q3DataTableVerdaderoTipo QUndoGroup"A6 (105 x 148 mm)&Copiar Q3TextEdit&Copiar QLineEdit&Copiar&Tipo de letra &Ayuda &Mover &Abrir Q3FileDialog &Abrir &Rojo:&Rehacer Q3TextEdit&Rehacer QLineEdit&Rehacer&Saturacin:&Guardar Q3FileDialog&Guardar&Tamao QFontDialog&Tamao&Deshacer Q3TextEdit&Deshacer QLineEdit&Deshacer&Brillo:NLegal (8,5 x 14 pulgadas, 216 x 356 mm)InterrumpirAplicar Q3TabDialogAplicar QCheckBoxLimpiar Cerrar Q3TitleBar CerrarQDialogButtonBox CerrarQMenu Cerrar Unidad Intro Falso Q3DataTable Falso Error &Tono:Minimizar Q3TitleBarMinimizar0No se ha podido abrir %1Imprimir todo Abrir  Q3FileDialog Abrir  PausaImpr Pant QPrintDialogImpr PantReintentarDerechaMayEspacio>La operacin de red ha expiradoLEl equpo remoto ha cerrado la conexinQu es esto?QDialogQu es esto?ZEl protocolo %1 no permite recibir ficherosPEl listado del directorio ha fallado: %1bNo se ha podido borrar el fichero o directorio %1Escritura: %1Grabar medio"B6 (125 x 176 mm) QDB2Driver QIBaseDriver QIBaseResult QMYSQLDriver QODBCDriverQSQLite2Driver(Equipo no encontradoQAbstractSocket(Equipo no encontrado<Descriptor de socket no vlido"Imprimir pantallaD&Definir colores personalizados >>"Fuente del papel:B8 (62 x 88 mm)treferencia a entidad no analizada en un contexto no vlidoA8 (52 x 74 mm)B9 (44 x 62 mm):Tipo de protocolo no admitidoA9 (37 x 52 mm)&Ordenar por &tamao Q3FileDialog&Ordenar por &tamao&B0 (1000 x 1414 mm)$Ordenar por &fecha Q3FileDialog$Ordenar por &fecha&Ordenar por &nombre Q3FileDialog&Ordenar por &nombreRFallo de la creacin de un directorio: %1LNo se ha podido crear el directorio %1Nueva carpeta 1PActiva la ventana principal del programa"A5 (148 x 210 mm).Tabloide (279 x 432 mm)HRLE Start of right-to-left embedding6Permanecer en &primer plano<Mo&strar este mensaje de nuevoBsintaxis no vlida para lookaheadno se permiten referencias a entidades externas generales ya analizadas en la DTD B10 (31 x 44 mm)2Mtodo de entrada Windows^valor errneo para la declaracin independienteRe&ducirNo conectado Bajar los agudosRConexin para conexin de datos rechazada*Mtodo de entrada XIMse esperaba una declaracin de codificacin o declaracin autnoma al leer la declaracin XML"B2 (500 x 707 mm)el directorio QDB2Result"&Tipo de fichero:&&Nombre de fichero:bNo hay ningn fichero o directorio con ese nombre&Cerrar4Identificacin fallida: %1 &Pegar Q3TextEdit &Pegar QLineEdit &Pegar4Bloqueo del desplazamiento*Desplazar hacia abajo(Desplazar hasta aqu8Desplazar hacia la izquierdaBorrar %1HFallo de la descarga del fichero: %1"A3 (297 x 420 mm) QDB2Result QMYSQLResult QOCIResult QODBCResultQSQLite2Result%1 - [%2]&Conectado al equipoQFtp&Conectado al equipoAlinear Q3MainWindowAlinear*ZWSP Zero width space Bajar el volumenSilenciarIntento de usar un socket IPv6 sobre una plataforma que no contempla IPv6"B4 (250 x 353 mm)>La plataforma no contempla IPv6Medio anterior>no se ha producido ningn errorQRegExp>no se ha producido ningn error|se ha producido un error durante el anlisis de una referencia"Formato del papel Subir el volumense esperaba una declaracin independiente al leer la declaracin XML"A1 (594 x 841 mm)*Seleccin de un colorH&Aadir a los colores personalizados,Conectado al equipo %1QFtp,Conectado al equipo %1 Ms...zEl protocolo %1 no permite renombrar ficheros o directoriosPLa conexin con el equipo ha fallado: %1 Az&ul:LTR FinalCancelar Q3FileDialogCancelarQ3ProgressDialogCancelar Q3TabDialogCancelar QColorDialogCancelarQDialogButtonBoxCancelar QFileDialogCancelar QPrintDialogCancelarQProgressDialogCancelarLanzar (6)Lanzar (7)Lanzar (8)Lanzar (9)Lanzar (2)Lanzar (3)Lanzar (4)Lanzar (5)Lanzar (0)Lanzar (1)Lanzar (F)Lanzar (B)Lanzar (C)Lanzar (D)Lanzar (E)Lanzar (A).Equipo %1 no encontradoQFtp.Equipo %1 no encontrado Borrar Q3DataTable Borrar QLineEdit Borrar QShortcut BorrarQSql BorrarApaisado EscapeR&ecargarNueva carpetaDescolgar&Directorio superiorEl protocolo %1 no permite listar los ficheros de un directorio$B1 (707 x 1000 mm)FEl envo del fichero ha fallado: %1IgnorarInsertar Q3DataTableInsertar QShortcutInsertar(Folio (210 x 330 mm)(desconocido)(Potenciar los graves QIBaseDriver Av Pg$DLE (110 x 220 mm)RetornoMuestraSombre&arBsquedaSeleccionar&Sin ordenar Q3FileDialog&Sin ordenar$C5E (163 x 229 mm)NB5 (176 x 250 mm, 6,93 x 9,84 pulgadas) PetSis<falta el delimitador izquierdoBloq NumActualizar Q3DataTableActualizarBuscar &en:,Borrar este registro?8Imposible recibir un mensajeMi computadoraHContenido del fichero previsualizado$Imprimir intervaloBloq DesplDFallo del cambio de directorio: %1 Permiso denegado QIODevice Permiso denegadoContexto1Contexto2Contexto3Contexto4Buscar en:\nombre de instruccin de tratamiento no vlido Nueva carpeta %1"Equipo encontradoQFtp"Equipo encontrado"Nmero de copias:"Retroceder pgina.Una pgina hacia arriba QScrollBar.Una pgina hacia arriba4error debido al consumidor4Mtodo de entrada Mac OS XVertical QScrollBarVsintaxis no vlida para clase de caracteresvse ha producido un error durante el anlisis de un elemento$Conexin rechazadaQAbstractSocket$Conexin rechazadaQHttp$Conexin rechazadaAbrir URL QDB2Result QDB2Result QMYSQLResult QOCIResult8Guardar las modificaciones?$Imposible escribir&carcter inesperado,Mensaje de depuracin:JEliminacin de directorio fallida: %1.etiqueta desequilibrada"Lectura-escritura*Bloqueo de maysculas$Nombre de fichero:Slo lectura"Tamao de pgina:.Una pgina a la derecha QScrollBar.Una pgina a la derechaN&o a todoXse ha usado una caracterstica no habilitada$A0 (841 x 1189 mm),Solicitud HTTP fallida2&Estilo del tipo de letra>Imprimir en color si es posible2Copiar o mover un fichero(conectado localmenteT<qt>Seguro que desea borrar %1 %2?</qt>*Ledger (432 x 279 mm)*valor octal no vlido"Ficheros de tipo:(Peticin del sistemaJNo se ha podido leer el directorio %1Bloq Mays*Tabulador hacia atrs Subir los graves Aviso:*Sistema de escr&ituraPersonalizar...Actualizar&Restaurartse ha producido un error durante el anlisis del contenidozse ha producido un error durante el anlisis de un comentariono se permiten referencias a entidades internas generales en la DTD.Buscar en el directorioAtributos:Direccin de tipo desconocidobYa hay otro socket escuchando por el mismo puerto QMYSQLResult:Mostrar los ficheros &ocultos Q3FileDialog:Mostrar los ficheros &ocultos6Imposible enviar un mensajeHInsertar carcter de control Unicode,Desplazar hacia arribatEl protocolo %1 no permite borrar ficheros o directoriosAcerca de QtHLa direccin enlazada ya est en usoGuardar como Q3FileDialogGuardar comoAlias: %1%1 Directorio no encontrado. Verique que el nombre del directorio es correcto."Conexin expiradaError de redInaccesible QIBaseResultFLRO Start of left-to-right overrideHLRE Start of left-to-right embedding%1 Fichero no encontrado. Compruebe la ruta y el nombre del fichero.Lanzar correo Borrar(entidades recursivas8Seleccionar un tipo de letraDSobre US Common #10 (105 x 241 mm) Bajar los graves8Operacin socker no admitida QIBaseDriverConfirmarVista de lista Q3FileDialogVista de lista>Conexin rechazada al equipo %18Operacin socket no admitidaTms de una definicin de tipo de documento Fichero especialBOperacin detenida por el usuarioXEl protocolo %1 no permite enviar ficherosSlo escritura&CancelarQ3Wizard&Cancelar&el enlace simblico QMYSQLResult Reposo&Aumentar&Borrar Q3FileDialog&BorrarActivarS&ubrayado&Terminar*Conexin a %1 cerradaQFtp*Conexin a %1 cerradaDsintaxis no vlida para repeticin&Verde:,Todos los ficheros (*) Q3FileDialog,Todos los ficheros (*)Directorios Q3FileDialogDirectoriosDEliminacin de fichero fallida: %1LEnlace simblico a un fichero especialVImposible inicializar el socket de difusinlEl ejecutable %1 requiere Qt %2 (se encontr Qt %3).2Seleccionar un directorio Q3FileDialog2Seleccionar un directorioSiguie&nte >S a &todoDNo queda espacio en el dispositivo0Todos los ficheros (*.*) Q3FileDialog0Todos los ficheros (*.*)bse esperaba la versin al leer la declaracin XMLLectura: %1&Recargar&Renombrar Q3FileDialog&Renombrar Subir los agudosBNo se ha podido renombrar %1 a %2NA4 (210 x 297 mm, 8,26 x 11,7 pulgadas) Seleccionar todo Q3TextEdit Seleccionar todo QLineEdit Seleccionar todoDirectorio: Q3FileDialogDirectorio:&Valores por omisinEfectos&Tachado4Desplazar hacia la derecha0Fragmento HTTP no vlidoBloq num(Equipo %1 encontradoQFtp(Equipo %1 encontrado Bloqueo numrico<Longitud del contenido errnea Colores &bsicos*se esperaba una letra"Error desconocidoQFtp"Error desconocido QHostInfo"Error desconocidoQHostInfoAgent"Error desconocidoQHttp"Error desconocido QIODevice"Error desconocidoHLa secuencia %1, %2 no est definidaDesconocidodesconocido:Cancelar sus modificaciones?Q&uitar sombra,RLM Right-to-left markZEl fichero %1 ya existe. Desea reemplazarlo?.Colores &personalizados2fin de fichero inesperado QDB2Driver QMYSQLDriver QODBCDriverVEjecutivo (7,5 x 10 pulgadas, 191 x 254 mm)ZImposible inicializar el socket no bloqueante<PDF Pop directional formatting QDB2Driver QIBaseDriver QMYSQLDriver6La direccin est protegida6No se ha podido escribir %18se alcanz el lmite internoHCabecera de respuesta HTTP no vlidaError de expiracioD socks5 mientras se estableca una conexin al servidor socksCanal a&lfa:no se permiten referencias a entidades externas generales ya analizadas en el valor de un atributoMi&nimizarBImprimir primero la ltima pginaBorde izquierdoNCarta (8,5 x 11 pulgadas, 216 x 279 mm)FavoritosSiguienteAvanzar pgina,Una pgina hacia abajo QScrollBar,Una pgina hacia abajo2Una pgina a la izquierda QScrollBar2Una pgina a la izquierda Conexin cerradaQFtp Conexin cerrada4ZWNJ Zero width non-joiner2Ir al directorio superiorLInformacin del fichero previsualizadoMa&ximizarRestaurar abajo>Secuencia ambigua %1 no tratada2Operacin socket expiradaTipo< &Anterior Red inalcanzablefNo se ha indicado ningn servidor al que conectarseFRLO Start of right-to-left overridenerror en la declaracin de texto de una entidad externael ficherofin inesperadoZEl servidor cerr la conexin inesperadamenteXDemasiados ficheros abiertos simultneamenteJEl protocolo %1 no est contemplado*ZWJ Zero width joiner>La direccin no est disponible"B3 (353 x 500 mm).Crear una nueva carpeta Q3FileDialog.Crear una nueva carpeta:Enlace simblico a un fichero,LRM Left-to-right mark&Reproducir el medio Detener el medio,Insuficientes recursosSiguiente medioEl protocolo %1 no permite copiar o mover ficheros o directorios@Enlace simblico a un directorio,Solicitud interrumpidase ha producido un error durante el anlisis de la definicin de tipo de documentob<p>Este programa utiliza la versin %1 de Qt.</p>Borde derechojEl protocolo %1 no permite crear nuevos directoriosLanzar medio A7 (74 x 105 mm)Vista detallada Q3FileDialogVista detallada%1 Fichero no encontrado. Verifique que el nombre del fichero es correcto."A2 (420 x 594 mm)Orientacin: B7 (88 x 125 mm)BError: biblioteca Qt incompatible QMYSQLDriverQSQLite2Driver/z "(/8=CFKR\ahu{ $,9AHORenw '-39@ QProgressBarQDialog QDB2Driver QCheckBox QUndoStackQXml Q3TitleBar QMYSQLResultQDialogButtonBoxQ3Accel Q3TextEditQFtpQLibrary QFontDialogQMultiInputContextQRegExp QODBCResultQMultiInputContextPlugin QDB2Result QODBCDriver QDirModel QTcpServer QTDSDriver Q3FileDialog QSQLiteResultQSQLite2Result QToolButton QScrollBarQNativeSocketEngine Q3LocalFsQSlider QTextControl QPSQLDriver QColorDialog QIODevice QMYSQLDriver QAxSelect QWorkspace QApplication QOCIResult QShortcutQAbstractSpinBox QErrorMessage QSQLiteDriverQHostInfoAgent QUdpSocket QRadioButton QDateTimeEdit QMessageBox Q3TabDialogQSqlQTabBar QFileDialogQ3ProgressDialogQProgressDialogPPDOptionsModelQ3NetworkProtocolQMenu Q3MainWindowQWhatsThisAction QPrintDialogQUnicodeControlCharacterMenuQSQLite2DriverQObjectQPrintPropertiesDialog QComboBoxQ3Wizard Q3UrlOperator QInputContextQHttp QIBaseDriver Q3ToolBar QHostInfo QIBaseResultQWidget QLineEdit Q3DataTableQAbstractSocket QPSQLResultQSocks5SocketEngine QUndoModel QOCIDriver QUndoGroup QPushButtonQSpinBoxTreeLine/translations/treeline_pt.qm0000644000175000017500000024051413715363644016657 0ustar dougdougcg.AhDm3=x9e'VkW,j%6D0^^10KpW78[H5H5U;VE c ^/c2r5c`+e-d¬]E7~*h*yϞ***%*T*Eu*0*5i'+Ub+ҳ+++/+Gu+:+į*+/55S4 T;0$avS"'}Bw9w9v66wDԳ7ggc zCf6 {3 f3UP[y)$w%oMoC:U(]nﶰ~}6seyRy3ŏ|c %v>-$6nn6nb6n G^[R5<_ fc g|ۇ] #7/%U<nZ g̓wiaei~b~<~CjJu O4/%Yy7ctPW~הY#99ٞ@mD+NR p[ó7aLWXf@qgxi 5lwpѡoFszF}'.~oI.CtJz$@HF_VoZ F <$§#³:jGh w7o^|<.AU T.&=!L(M+5>?d@{$FJ\8\`}f4gNeh/rU|3s}8u`cPgG%5C~cQRJT(N!."k*+%["D9ϑ&Į0eT)שD,Y:KgDkH|S!5mcmtd-[ѤT~nJUn#CH#Cm*,9.//iW/0?,In3feL RSS"v+ZzZh]_Pb#|e,ffȊtglYr^(sPuh[ITi'.w~ |Ljϥ1M..R@v˃wΨ%@C7i_}E\>_f"^^4,.&k Y&t 2)&/0ndo7OT8OT!CdiP[FU'hSk#kNnMCnnD}\oj؎LN/ A!? [BeǨOe"upE(3p^ EfOdgųOa ; 'J,01cD1cD9Hks@ElFyLCf$W.[%d]!n#9wƾ{0>E !ccic$ IHnjT©&M~*)a9~o1 ^J !6%%z}%%xṄ/9:%{UKԤ?`޹߂_ߗevS+l^\m N \~7 \~m ;6h q#*s 4Eo 5f :ro Lx Mg f+>U k 8@  # ωsy ? b*8 ld| b   ʵ tX{ n 9 8 Y cY a  .t _ Tg W4Z ,_$. /2_ 4 5r 7* = ITR Ty  Wܾ5 _҉& ap cD c e ( uo u5 w5y; ăR_ 5A 1 : I I I I? Iv I I IP C] 9VN  m 6" 5 % 3w ^ I ; 2# t ˈ(= ĽNz ;: Gc! :q :w E/ w ;+ 5 e3 { : =  s .z& Q t{ Xf KA )M -8h .2& /. 2  5 @(e AZ K&= L. L9 W X [t# rWr x( C x( :32 u, sx g H   7 Zy i ;Q DG ( G D' B ~ t ٷ ߺ KG ~ \E#e X y~ 8 vs n N E AX +@\ P" % #: Q3 U '{ (ECF 7"* 8Ǒ\ ;y7 =: @E V dMY8 g#1 pk t" t$ t` v"; {B~3 =FD  eq h- MQ T^ B   C e m) ү\ vW bnT D U6  Y \L B 't . 0uI 0. 7y6 C ! G9. H4 Kz9m8 K M ]`^W _ _ dT& i2 i$h i$xQ z h 2 Q ݃ |@ =/k c' Dg sH ~t` >`-"J`s"sKZ(t-k/+0/Sy0>S+97= # EjVH;eL`#Lh?LeOL^V5Y&Zrjhj`kDlfc\p#Cq2tD -]txb tՋ:8ke-`EԕYe-h~ ;tp|VCg`;> uKc mR( 6t8 qa<s@-HNSI?M&UZ8n@[Ke3dem3J iFW=r^s$Lww }$74?y*A Բ6t$^DBpu2$&J|ɟ)];ݮo8d7F\3f6Yfb9i(;&Cancelar&Cancelcolorset&OK&OKcolorset*Adicionar &Nova Regra &Add New Rule conditional&Cancelar&Cancel conditional&Fechar&Close conditional&&Terminar Filtragem &End Filter conditionalF&iltrar&Filter conditional&OK&OK conditional&Eliminar Regra &Remove Rule conditional&Guardar&Save conditional FalsoFalse conditional&Encontrar &Seguinte Find &Next conditional&Encontrar &AnteriorFind &Previous conditionalLNenhum resultado satisfaz as condies!No conditional matches were found conditionalTipo de ramo Node Type conditionalRegra {0}Rule {0} conditionalVerdadeiroTrue conditional [Todos os Tipos] [All Types] conditionaleand conditional contmcontains conditionalacaba com ends with conditionalouor conditionalcomea com starts with conditional&Aplicar&Apply configdialog&Cancelar&Cancel configdialogTipo de &Dados &Data Type configdialog&Eliminar Tipo &Delete Type configdialog,&Derivadar de Original&Derive from original configdialog&Equao &Equation configdialog.C&onfigurao de Campos &Field Config configdialogT&ipo de Campo &Field Type configdialog$&Ocultar Avanadas&Hide Advanced configdialog"Mover para &Baixo &Move Down configdialog&Novo Campo... &New Field... configdialog&Novo Tipo... &New Type... configdialog&OK&OK configdialog&Prefixo&Prefix configdialog &Repor&Reset configdialog&&Tipo de Resultados &Result Type configdialog"&Seleccionar Tudo &Select All configdialog$&Mostrar Avanadas&Show Advanced configdialog0&Critrio de Ordenamento&Sort Criteria configdialog*&Formatao do Ttulo &Title Format configdialogHAdicionar linhas &vazias entre ramosAdd &blank lines between nodes configdialogAdicionar Campo Add Field configdialogAdicionar TipoAdd Type configdialogFAdicionar ou Remover Tipos de DadosAdd or Remove Data Types configdialog6Adicionar &pontos de tpicoAdd text bullet&s configdialogBPermitir texto formatado em &HTMLAllow &HTML rich text in format configdialog,Operadores AritmticosArithmetic Operators configdialog"Tipos AutomticosAutomatic Types configdialog8Lista de &Campos DisponveisAvailable &Field List configdialog&Campos &DisponveisAvailable &Fields configdialog"Resultado BoleanoBoolean Result configdialogvNo possivel apagar tipo de dados em utilizao por ramos+Cannot delete data type being used by nodes configdialogMudar &cone Change &Icon configdialog,Nmero de Descendentes Child Count configdialog2Referncia a DescendentesChild Reference configdialog &Limpar Seleco Clear &Select configdialogCo&piar Tipo... Co&py Type... configdialogzCarcter de &Separao de Combinaes e Lista de Descendentes+Combination && Child List Output &Separator configdialog0Operadores de ComparaoComparison Operators configdialog2Configurar Tipos de DadosConfigure Data Types configdialogCopiar Tipo Copy Type configdialog2&Criar Tipos CondicionaisCreate Co&nditional Types configdialog"Resultado de Data Date Result configdialogHValor pr &definido para novos ramosDefault &Value for New Nodes configdialogBTipo de &descendente pr definidoDefault Child &Type configdialogDefinir EquaoDefine Equation configdialogFDefinir equao do campo matemticoDefine Math Field Equation configdialog&Eliminar Campo Dele&te Field configdialog4&Derivado de Tipo GenricoDerived from &Generic Type configdialogDescrio Description configdialogDireco Direction configdialog:Altura da Caixa de Introduo Editor Height configdialog>Introduza o nome do novo campo:Enter new field name: configdialog@Introduza novo nome para o tipo:Enter new type name: configdialog&Erro na equao: {}Equation error: {} configdialogvErro - referncia cclica em equaes de campos matemticos2Error - circular reference in math field equations configdialogTexto adicional Extra Text configdialog Ca&mpoF&ield configdialog Li&sta de Campos F&ield List configdialog CampoField configdialog Lista de &Campos Field &List configdialog(Referncias a CamposField References configdialog6Informao sobre o ficheiroFile Info Reference configdialog$&Inverter DirecoFlip &Direction configdialog"&Ajuda de Formato Format &Help configdialog coneIcon configdialog$Equao Matemtica Math Equation configdialog4&Modificar Lista de CamposModify &Field List configdialog:&Modificar Tipos CondicionaisModify Co&nditional Types configdialog Mover para &CimaMove &Up configdialog"Mover para &Baixo Move Do&wn configdialog Mover para &cimaMove U&p configdialogNomeName configdialog NenhumNone configdialog4&Nmero de linhas de textoNum&ber of text lines configdialog"&Tipo de OperadorO&perator Type configdialog&ApresentaoO&utput configdialog(&Lista de OperadoresOper&ator List configdialogOperaes Operations configdialog4Referncias a outros campoOther Field References configdialog6&Formatao de ApresentaoOut&put Format configdialog0&Formato de ApresentaoOutpu&t Format configdialog,Opes de apresentaoOutput Options configdialog0Referncia a AscendentesParent Reference configdialog&&Tipo de RefernciaRefere&nce Type configdialog(&Nvel de RefernciaReference &Level configdialog&&Tipo de RefernciaReference &Type configdialog(&Nvel de RefernciaReference Le&vel configdialogM&udar Nome...Rena&me Field... configdialog &Mudar o Nome...Rena&me Type... configdialogMudar Nome Rename Field configdialog"Mudar Nome a Tipo Rename Type configdialog,Mudar nome de {} para:Rename from {} to: configdialog"Referncia RaizRoot Reference configdialogAuto RefernciaSelf Reference configdialog<Definir cone do Tipo de DadosSet Data Type Icon configdialog<Definir tipos condicionalmenteSet Types Conditionally configdialog:&Prioridade de Ordenamento... Sort &Keys... configdialog2Prioridade de OrdenamentoSort Key configdialog*Campos de OrdenamentoSort Key Fields configdialog&SufixoSuffi&x configdialog&Lista de Tipos T&ype List configdialog&Operadores de TextoText Operators configdialog"Resultado Textual Text Result configdialog\Os seguintes carcteres no so permitidos: {},The following characters are not allowed: {} configdialog2O nome no pode ser vazioThe name cannot be empty configdialog<O nome no pode conter espaosThe name cannot contain spaces configdialogBO nome no pode comear com "xml" The name cannot start with "xml" configdialogBO nome deve comear com uma letra!The name must start with a letter configdialog*Nome j em utilizaoThe name was already used configdialog$Resultado de Tempo Time Result configdialog,Configurao de &Tipos Typ&e Config configdialogTipoType configdialogT&Utilizar uma tabela para dados dos camposUse a table for field &data configdialog[Nenhum][None] configdialogvalor absolutoabsolute value configdialogadicionaradd configdialogarco coseno arc cosine configdialogarco senoarc sine configdialogarco tangente arc tangent configdialog mdiaaverage configdialog(logartmo de base 10base-10 logarithm configdialog concatenar textoconcatenate text configdialog>converter texto para minsculasconvert text to lower case configdialog>converter texto para maisculasconvert text to upper case configdialog$coseno de radianoscosine of radians configdialog&graus para radianosdegrees to radians configdialogdividirdivide configdialogigual aequal to configdialogfactorial factorial configdialogponto flutuantefloating point configdialog&Diviso arredondada floor divide configdialogDescendenteforward configdialogDescendentefwd configdialogmaior que greater than configdialogmaior ou igualgreater than or equal to configdialog inteiro superiorhigher integer configdialogxno 1 argumento, substituir o 2 argumento pelo 3 argumento(in 1st arg, replace 2nd arg with 3rd arg configdialogvjuntar texto utilizando o primeiro argumento como separador$join text using 1st arg as separator configdialogmenor que less than configdialog menor ou igual aless than or equal to configdialoge lgico logical and configdialogou lgico logical or configdialog inteiro inferior lower integer configdialog mximomaximum configdialog mnimominimum configdialog restomodulus configdialogmultiplicarmultiply configdialog8constante logartmo natural natural log constant configdialog"logartmo naturalnatural logarithm configdialogdiferente de not equal to configdialogconstante pi pi constant configdialogpotnciapower configdialog&radianos para grausradians to degrees configdialogAscendenterev configdialogAscendentereverse configdialog2arredondar para x dgitosround to num digits configdialog seno de radianossine of radians configdialograiz quadrada square root configdialogsubtrairsubtract configdialogsumatrio sum of items configdialog(tangente de radianostangent of radians configdialogzverdadeiro se o 1 argumento de texto contiver o 2 argumento%true if 1st text arg contains 2nd arg configdialogverdadeiro se o 1 argumento de texto terminar com o 2 argumento&true if 1st text arg ends with 2nd arg configdialogverdadeiro se o 1 argumento de texto comear com o 2 argumento(true if 1st text arg starts with 2nd arg configdialogNvalor verdadeiro, condio, valor falso"true value, condition, false value configdialog inteiro truncadotruncated integer configdialog$Procurar &Ficheiro&Browse for File dataeditors&Cancelar&Cancel dataeditors &Ir para Destino &Go to Target dataeditors&OK&OK dataeditors&Abrir Ligao &Open Link dataeditors&Abrir Imagem &Open Picture dataeditorsAbsolutoAbsolute dataeditorsEndereoAddress dataeditors*Texto de Apresentao Display Name dataeditorsLigao Externa External Link dataeditors>Tipo de Caminho para o FicheiroFile Path Type dataeditorsLigao Interna Internal Link dataeditorsAbrir &Pasta Open &Folder dataeditors&Ligao para imagem Picture Link dataeditorsRelativoRelative dataeditorsProtocoloScheme dataeditorsData de &Hoje Today's &Date dataeditorsPTreeLine - Ligao para Ficheiro ExternoTreeLine - External Link File dataeditors:TreeLine - Ficheiro de ImagemTreeLine - Picture File dataeditors&Colunas&Columnsexportsrvore &Inteira &Entire treeexports &HTML&HTMLexports4Favoritos em formato &HTML&HTML format bookmarksexportsndice &ODF &ODF Outlineexports>&Apenas descendentes expandidos&Only open node childrenexports$HTML Pgina &nica&Single HTML pageexports@&Ttulos separados por tabulao&Tabbed title textexports &Texto&TextexportsXContedo no &formatado para todos os textos&Unformatted output of all textexports4Favoritos em formato &XBEL&XBEL format bookmarksexports&Favoritos Book&marksexportsFavoritos BookmarksexportsTEscolha o subtipo do formato de exportaoChoose export format subtypeexports>Escolha o formato de exportaoChoose export format typeexports>Escolha as opes de exportaoChoose export optionsexports.Exportao de ficheiros File ExportexportsPIncluir &cabealho e rodap de impressoInclude &print header && footerexportsbMultiplas pginas HTML com &tabelas de informaoMultiple HTML &data tablesexports\&Multiplas pginas HTML com barra de navegao)Multiple HTML &pages with navigation paneexports:&Nveis da barra de navegaoNavigation pane &levelsexportsOutras Opes Other Optionsexports.&Subramos SeleccionadosSelected &branchesexports(&Ramos SeleccionadosSelected &nodesexportsRHTML pgina nica com barra de &navegao&Single &HTML page with navigation paneexports@TreeLine - Exportar XML GenricoTreeLine - Export Generic XMLexports0TreeLine - Exportar HTMLTreeLine - Export HTMLexportsJTreeLine - Exportar Favoritos em HTML TreeLine - Export HTML Bookmarksexports:TreeLine - Exportar Texto ODFTreeLine - Export ODF TextexportsBTreeLine - Exportar Texto SimplesTreeLine - Export Plain TextexportsHTreeLine - Exportar Tabelas de TextoTreeLine - Export Text TablesexportsHTreeLine - Exportar Texto de TtulosTreeLine - Export Text TitlesexportsNTreeLine - Exportar Sub rvore TreeLine"TreeLine - Export TreeLine SubtreeexportsJTreeLine - Exportar Favoritos em XBEL TreeLine - Export XBEL BookmarksexportsO que exportarWhat to ExportexportsCarcter "." .."." Character .. fieldformatCarcter "/" //"/" Character // fieldformat&0 Ou 1 Repeties ?0 Or 1 Repetitions ? fieldformat,0 ou Mais Repeties *0 Or More Repetitions * fieldformat,1 Ou Mais Repeties +1 Or More Repetitions + fieldformat&Qualquer carcter .Any Character . fieldformatAutoEscolha AutoChoice fieldformatAutoCombinaoAutoCombination fieldformatBoleanoBoolean fieldformat"Letra Maiscula ACapital Letter A fieldformat4Numeral Romano Maisculo ICapital Roman Numeral I fieldformatEscolhaChoice fieldformatCombinao Combination fieldformatDataDate fieldformat"Vrgula Decimal ,Decimal Comma , fieldformatPonto Decimal .Decimal Point . fieldformatDDgito ou espao (externo) <space>!Digit or Space (external)  fieldformatFim de texto $ End of Text $ fieldformat8Terminar Carcter Especial \Escape a Special Character \ fieldformatExemplo 1/2/3/4Example 1/2/3/4 fieldformat.Exponente (maiscula) EExponent (capital) E fieldformat.Exponente (minscula) eExponent (small) e fieldformatLigaoExterna ExternalLink fieldformatTextoHTMLHtmlText fieldformatLigaoInterna InternalLink fieldformat(Separador de Nvel /Level Separator / fieldformat.Letras Minsculas [a-z]Lower Case Letters [a-z] fieldformatMatemticaMath fieldformat8Carcter no numrico [^0-9]Not a Number [^0-9] fieldformat AgoraNow fieldformat NmeroNumber fieldformatNmero 1Number 1 fieldformatNumerao Numbering fieldformatTextoLinhaUnica OneLineText fieldformat"Dgito Opcional #Optional Digit # fieldformat Sinal Opcional -Optional Sign - fieldformatOu |Or | fieldformatFExemplo de ndice I../A../1../a)/i)!Outline Example I../A../1../a)/i) fieldformat ImagemPicture fieldformat ExpressoRegularRegularExpression fieldformat(Dgito Obrigatrio 0Required Digit 0 fieldformat&Sinal Obrigatrio +Required Sign + fieldformat2Exemplo de Seco 1.1.1.1Section Example 1.1.1.1 fieldformat*Separador de Seco .Section Separator . fieldformatSeparador / Separator / fieldformat2Conjunto de Nmeros [0-9]Set of Numbers [0-9] fieldformat"Letra Minscula aSmall Letter a fieldformat4Numeral Romano Minsculo iSmall Roman Numeral i fieldformatJEspao de Separao (interno) <space>"Space Separator (internal)  fieldformatTextoEspaado SpacedText fieldformatV/FT/F fieldformat TextoText fieldformat TempoTime fieldformat.Letras Maisculas [A-Z]Upper Case Letters [A-Z] fieldformatS/NY/N fieldformat verdadeiro/falso true/false fieldformatsim/noyes/no fieldformat falsofalse genbooleannono genbooleanverdadeirotrue genbooleansimyes genboolean$Todos os Ficheiros All Files globalrefFicheiros HTML HTML Files globalrefFicheiros ODFODF Text Files globalrefFicheiros PDF PDF Files globalref$Ficheiros de Texto Text Files globalref$Ficheiros TreeLineTreeLine Files globalref@Ficheiros TreeLine - ComprimidosTreeLine Files - Compressed globalref@Ficheiros TreeLine - EncriptadosTreeLine Files - Encrypted globalref"Ficheiros Treepad Treepad Files globalrefFicheiros XML XML Files globalrefEncontrar:  Find: helpview&Retroceder&Backhelpview&Avanar&Forwardhelpview&Incio&Homehelpview&Encontrar &Seguinte Find &Nexthelpview&Encontrar &AnteriorFind &Previoushelpview(Texto no encontradoText string not foundhelpviewFerramentasToolshelpview"{0}" no um ficheiro TreeLine vlido. Utilizar um filtro de importao?:"{0}" is not a valid TreeLine file. Use an import filter?importsf&XML Genrico (ficheiro no especfico do TreeLine) &Generic XML (non-TreeLine file)importsHFavoritos em &HTML (Formato Mozilla) &HTML bookmarks (Mozilla Format)imports^&Texto recuado por tabulao, um ramo por linha%&Tab indented text, one node per lineimports:&Favoritos XML (Formato XBEL)&XML bookmarks (XBEL format)importsFAVORITOBOOKMARKimportsFavoritos Bookmarksimports:Escolher Mtodo de ImportaoChoose Import MethodimportsTErro - no foi possvel ler o ficheiro {0}Error - could not read file {0}importsPErro - formato de ficheiro imprprio {0}Error - improper format in {0}imports PASTAFOLDERimports"Importar Ficheiro Import Fileimports"Ficheiro Invlido Invalid FileimportsLigaoLinkimports6ndice &Open Document (ODF)Open &Document (ODF) outlineimports|&Pargrafos de texto simples (delimitados por linha em branco)-Plain text ¶graphs (blank line delimited)importsSEPARADOR SEPARATORimports TABELATABLEimportsTabela de texto delimitada por tabulao com &cabealhos e colunas)Tab delimited text table with header &rowimports TextoTextimports8TreeLine - Importar FicheiroTreeLine - Import FileimportsR&Ficheiro Treepad (ramos de texto apenas)Treepad &file (text nodes only)importsvReferncias a descendentes devem ser combinadas numa funo/Child references must be combined in a functionmatheval0Carcteres "{0}" ilegaisIllegal "{}" charactersmatheval6Funo ilegal presente: {0}Illegal function present: {0}mathevalNOperador ou tipo de objecto ilegal: {0}$Illegal object type or operator: {0}matheval2Sintaxe ilegal na equaoIllegal syntax in equationmatheval&Aplicar&Apply miscdialogs&Cancelar&Cancel miscdialogs&Fechar&Close miscdialogs &Terminar Filtro &End Filter miscdialogs&Toda a rvore &Entire tree miscdialogs&Filtrar&Filter miscdialogs&Encontrar &Seguinte &Find Next miscdialogs&Ascendente&Forward miscdialogs"&Ignorar e saltar&Ignore and skip miscdialogs0&Palavras chave parciais &Key words miscdialogs&Tipo de ramo &Node Type miscdialogs&OK&OK miscdialogsH&Campos de ordenamento pr definidos&Predefined Key Fields miscdialogs$&Expresso regular&Regular expression miscdialogs&Substituir&Replace miscdialogsP&Reiniciar contagem para prximos irmos"&Restart numbers for next siblings miscdialogs2&Restaurar pr definies&Restore Defaults miscdialogs&Descendente&Reverse miscdialogs &Pesquisar Texto &Search Text miscdialogs2&Descendentes da Seleco&Selection's children miscdialogsApenas &Ttulos &Titles only miscdialogs,&Barras de Ferramentas &Toolbars miscdialogs@&Tratar campos vazios como zeros&Treat blank fields as zeros miscdialogs@Utilizar &compresso de ficheiro&Use file compression miscdialogsNUtilizar fonte pr definida do &sistema&Use system default font miscdialogs--Separador-- --Separator-- miscdialogs*Comandos &DisponveisA&vailable Commands miscdialogs(&Qualquer ocorrncia Any &match miscdialogs&Remover Atalho Clear &Key miscdialogsBPersonalizar Barra de FerramentasCustomize Toolbars miscdialogsInformaoData miscdialogsMenu Informao Data Menu miscdialogsFPr definido - Texto em linha nicaDefault - Single Line Text miscdialogs EditarEdit miscdialogsMenu Editar Edit Menu miscdialogs@Tipo de Letra da Vista de EdioEditor View Font miscdialogsHPalavra Passe de Ficheiro EncriptadoEncrypted File Password miscdialogsBErro - Expresso Regular invlida"Error - invalid regular expression miscdialogs4Erro - substituio falhouError - replacement failed miscdialogs&Frase completa F&ull phrase miscdialogs CamposFields miscdialogsFicheiroFile miscdialogsMenu Ficheiro File Menu miscdialogs0Propriedades do FicheiroFile Properties miscdialogs2Armazenamento do Ficheiro File Storage miscdialogsFiltrarFilter miscdialogsEncontrarFind miscdialogs&Encontrar &Seguinte Find &Next miscdialogs&Encontrar &AnteriorFind &Previous miscdialogs,Encontrar e SubstituirFind and Replace miscdialogs$Toda a &informao Full &data miscdialogs&Palavras &Completas Full &words miscdialogsVCritrio para Ramos sem Campos de Numerao'Handling Nodes without Numbering Fields miscdialogs AjudaHelp miscdialogsMenu Ajuda Help Menu miscdialogsComo pesquisar How to Search miscdialogsDIcluir ramos dos nveis superioresInclude top-level nodes miscdialogs2Palavras chave &completasKey full &words miscdialogs4Tecla {0} j em utilizaoKey {0} is already used miscdialogs$Atalhos de TecladoKeyboard Shortcuts miscdialogsXCdigo de linguagem ou dicionrio (opcional)&Language code or dictionary (optional) miscdialogscones Grandes Large Icons miscdialogs$Campos Matemticos Math Fields miscdialogs"Mover para &Baixo Move &Down miscdialogs Mover para &CimaMove &Up miscdialogs"&Campos dos Ramos N&ode Fields miscdialogsxNo foram encontrados campos de numerao nos tipos de dados,No numbering fields were found in data types miscdialogsRamoNode miscdialogs$Ttulos dos &Ramos Node &Titles miscdialogsMenu Ramo Node Menu miscdialogs:Tipo de Letra de ApresentaoOutput View Font miscdialogs$&Expresso regularRe&gular expression miscdialogs:Re-Introduza a Palavra Passe:Re-Type Password: miscdialogsTPalavras passe introduzidas so diferentesRe-typed password did not match miscdialogsVLembrar a palavra passe durante esta sesso%Remember password during this session miscdialogs"Substituir &Todos Replace &All miscdialogsDForam substituidas {0} ocorrnciasReplaced {0} matches miscdialogs,Texto de &SubstituioReplacement &Text miscdialogs"&Reservar nmerosReserve &numbers miscdialogsRaiz Root Node miscdialogsPTermos de pesquisa "{0}" no encontradosSearch string "{0}" not found miscdialogsLTermo de pesquisa "{0}" no encontradoSearch text "{0}" not found miscdialogs(Ramos &SeleccionadosSelected &branches miscdialogs&&Irmos da selecoSelection's &siblings miscdialogs2&Descendentes da selecoSelection's childre&n miscdialogscones Pequenos Small Icons miscdialogs.Direco de OrdenamentoSort Direction miscdialogs*Mtodo de Ordenamento Sort Method miscdialogsOrdenar Ramos Sort Nodes miscdialogs(Verificar Ortografia Spell Check miscdialogsBComandos da &Barra de FerramentasTool&bar Commands miscdialogs@&Tamanho da Barra de Ferramentas Toolbar &Size miscdialogs>Nmero de Barras de FerramentasToolbar Quantity miscdialogsFerramentasTools miscdialogs Menu Ferramentas Tools Menu miscdialogs.Tipo de Letra da rvoreTree View Font miscdialogs$Numerao TreeLineTreeLine Numbering miscdialogsJIntroduza a palavra passe para "{0}":Type Password for "{0}": miscdialogs4Introduza a Palavra Passe:Type Password: miscdialogs<Actualizar Numerao dos RamosUpdate Node Numbering miscdialogsBUtilizar &encriptao de ficheiroUse file &encryption miscdialogsVerView miscdialogsMenu Ver View Menu miscdialogsOnde pesquisarWhat to Search miscdialogsO que Ordenar What to Sort miscdialogs O que ActualizarWhat to Update miscdialogs JanelaWindow miscdialogsMenu Janela Window Menu miscdialogsPPalavras passe vazias no so permitidas'Zero-length passwords are not permitted miscdialogs"[Todos os Campos] [All Fields] miscdialogs [Todos os Tipos] [All Types] miscdialogsNomeName nodeformatAparncia Appearanceoptiondefaults&Gravao Automtica Auto Saveoptiondefaults^Abrir automaticamente ltimo ficheiro utilizado!Automatically open last file usedoptiondefaultspRecuo dos descendentes (em unidades de altura da fonte) +Child indent offset (in font height units) optiondefaultsDClicar nos ramos para alterar nomeClick node to renameoptiondefaults6Formatos do Editor de DadosData Editor Formatsoptiondefaults DatasDatesoptiondefaults6Funcionalidades DisponveisFeatures Availableoptiondefaults,Primeiro dia da semanaFirst day of weekoptiondefaultsSexta-FeiraFridayoptiondefaultsJMinutos entre gravaes (0 desactiva)+Minutes between saves (set to 0 to disable)optiondefaultsSegunda-FeiraMondayoptiondefaults\Nmero de ficheiros recentes no menu ficheiro(Number of recent files in the file menuoptiondefaults6Nmero de passos a desfazerNumber of undo levelsoptiondefaults@Abrir ficheiros em novas janelasOpen files in new windowsoptiondefaults$Ficheiros Recentes Recent FilesoptiondefaultsBEditar nome de ramos aps criaoRename new nodes when createdoptiondefaultsLRepor a anterior disposio de janelas Restore previous window geometryoptiondefaultsrRestaurar o estado de visualizao dos ficheiros recentes(Restore tree view states of recent filesoptiondefaults SbadoSaturdayoptiondefaultsZMostrar descendentes na vista de apresentaoShow descendants in output viewoptiondefaultsBMostrar cones na vista em rvoreShow icons in the tree viewoptiondefaultsZMostrar campos matemticos na Vista de Edio&Show math fields in the Data Edit viewoptiondefaults\Mostrar campos de numerao na Vista de Edio+Show numbering fields in the Data Edit viewoptiondefaults$Opes de ArranqueStartup ConditionoptiondefaultsDomingoSundayoptiondefaultsQuinta-FeiraThursdayoptiondefaults TempoTimesoptiondefaultsZPermitir arrastar e largar na vista da rvoreTree drag && drop availableoptiondefaultsTera-FeiraTuesdayoptiondefaults(Memria de operaes Undo MemoryoptiondefaultsQuarta-Feira Wednesdayoptiondefaults&Cancelar&Canceloptions&OK&OKoptions`Escolher localizao do ficheiro de configurao"Choose configuration file locationoptionsXPasta do programa (para utilizao porttil)$Program directory (for portable use)optionsBPasta do utilizador (recomendado)#User's home directory (recommended)options8Erro ao iniciar a impressoraError initializing printer printdata.TreeLine - Exportar PDFTreeLine - Export PDF printdata &Base:&Bottom: printdialogs&Cancelar&Cancel printdialogsB&Desenhar linhas at descendentes&Draw lines to children printdialogs &rvore completa &Entire tree printdialogs &Fonte&Font printdialogs$Seleco de &Fonte&Font Selection printdialogs&Opes Gerais&General Options printdialogs,&Esquerda do Cabealho &Header Left printdialogs"&Cabealho/Rodap&Header/Footer printdialogs&Incluir Raiz&Include root node printdialogsZ&Manter primeiro descendente com o ascendente&Keep first child with parent printdialogs&Esquerda:&Left: printdialogs$&Nmero de colunas&Number of columns printdialogs&OK&OK printdialogs&Prefixo&Prefix printdialogs&Imprimir... &Print... printdialogs&Direita:&Right: printdialogs&Sufixo&Suffix printdialogs &Topo:&Top: printdialogs&Unidades&Units printdialogs\Utilizar a fonte de apresentao do &TreeLine &Use TreeLine output view font printdialogs&Largura:&Width: printdialogs"A3 (279 x 420 mm)A3 (279 x 420 mm) printdialogs"A4 (210 x 297 mm)A4 (210 x 297 mm) printdialogs"A5 (148 x 210 mm)A5 (148 x 210 mm) printdialogs>AaBbCcDdEeFfGg...TtUuVvWvXxYyZzAaBbCcDdEeFfGg...TtUuVvWvXxYyZz printdialogs Centmetros (cm)Centimeters (cm) printdialogsColunasColumns printdialogs*Tamanho personalizado Custom Size printdialogs$Fonte Pr Definida Default Font printdialogsTexto Extra Extra Text printdialogs(Pginas Consecutivas Facing Pages printdialogs OpesFeatures printdialogs&CamposFiel&ds printdialogs&&Formato dos Campos Field For&mat printdialogs6Formato de campo para "{0}"Field Format for "{0}" printdialogs Ajustar PginaFit Page printdialogs"Ajustar Largura Fit Width printdialogs &Estilo de Fonte Font st&yle printdialogs&Rodap:Foot&er: printdialogs&E&squerda do Rodap Footer &Left printdialogs$Ce&ntro do RodapFooter Ce&nter printdialogs$Direi&ta do Rodap Footer Righ&t printdialogs"&Ajuda de Formato Format &Help printdialogs&Cabealho:He&ader: printdialogs*&Direita do Cabealho Header &Right printdialogs(&Centro do CabealhoHeader C&enter printdialogs$Cabealho e RodapHeader and Footer printdialogsAltura:Height: printdialogsPolegadas (pol) Inches (in) printdialogsRamos IncludosIncluded Nodes printdialogs AvanoIndent printdialogsj&Distancia do Avano (em unidades de altura de linha)"Indent Offse&t (line height units) printdialogsAo &alto Lan&dscape printdialogs*Legal (8.5 x 14 pol.)Legal (8.5 x 14 in.) printdialogs*Carta (8.5 x 11 pol.)Letter (8.5 x 11 in.) printdialogsMargensMargins printdialogsMilmetros (mm)Millimeters (mm) printdialogsPgina Seguinte Next Page printdialogsPApenas descendentes de ramos &expandidosOnl&y open node children printdialogsOrientao Orientation printdialogs0Formato de &ApresentaoOutput &Format printdialogs.Configurao de &Pgina Page &Setup printdialogs"&Tamanho do Papel Paper &Size printdialogsAo &baixo Portra&it printdialogsPgina Anterior Previous Page printdialogsImprimirPrint printdialogs@&Previsualizao de Impresso...Print Pre&view... printdialogs8Previsualizao de Impresso Print Preview printdialogs.Definies de Impresso Print Setup printdialogs&Opes de impressoPrinting Setup printdialogsAmostraSample printdialogs"Seleccionar Fonte Select Font printdialogsF&Ramos e descendentes seleccionadosSelected &branches printdialogs(Ramos &SeleccioandosSelected &nodes printdialogs&TamanhoSi&ze printdialogsPgina nica Single Page printdialogs4&Espaamento entre colunasSpace between colu&mns printdialogs.Tablide (11 x 17 pol.)Tabloid (11 x 17 in.) printdialogsO que imprimir What to print printdialogsAproximarZoom In printdialogsAfastarZoom Out printdialogs&Adicionar&Add spellcheck&Cancelar&Cancel spellcheckIgnorar &Todas &Ignore All spellcheck&Substituir&Replace spellcheck0Adicionar em &MinsculasAdd &Lowercase spellcheckContexto:Context: spellcheckNo foi possvel encontrar aspell.exe, ispell.exe ou hunspell.exe Procurar localizao?QCould not find either aspell.exe, ispell.exe or hunspell.exe Browse for location? spellcheckBTerminada verificao ortogrficaFinished spell checking spellcheck&IgnorarIgnor&e spellcheck`Localizar aspell.exe, ispell.exe ou hunspell.exe-Locate aspell.exe, ipsell.exe or hunspell.exe spellcheck*Ausente no DicionrioNot in Dictionary spellcheck Programa (*.exe)Program (*.exe) spellcheck"S&ubstituir Todas Re&place All spellcheck.Verificao Ortogrfica Spell Check spellcheck>Erro de Verificao OrtogrficaSpell Check Error spellcheckSugestes Suggestions spellcheck@Verificao Ortogrfica TreeLineTreeLine Spell Check spellcheckErro de verificao Ortogrfica do TreeLine Cerifique-se que aspell, ispell ou hunspell esto instaladosLTreeLine Spell Check Error Make sure aspell, ispell or hunspell is installed spellcheckPalavra:Word: spellcheckPR DEFINIDODEFAULT treeformats coneIcon treeformats&Negrito &Bold Fonttreelocalcontrol&Copiar&Copytreelocalcontrol&Eliminar Ramo &Delete Nodetreelocalcontrol&Exportar... &Export...treelocalcontrol&&Ligao Externa...&External Link...treelocalcontrol"&Tamanho do texto &Font Sizetreelocalcontrol&Recuar Ramo &Indent Nodetreelocalcontrol&Itlico &Italic Fonttreelocalcontrol Mover para &Cima&Move Uptreelocalcontrol&Nova Janela &New Windowtreelocalcontrol C&olar&Pastetreelocalcontrol&Imprimir... &Print...treelocalcontrol&Refazer&Redotreelocalcontrol&Mudar Nome&Renametreelocalcontrol&Guardar&Savetreelocalcontrol*Definir &Tipo de Ramo&Set Node Typetreelocalcontrol0&Verificar Ortografia...&Spell Check...treelocalcontrol&Desfazer&UndotreelocalcontrolA&vanar Ramo&Unindent Nodetreelocalcontrol,Adicionar De&scendente Add &ChildtreelocalcontrolB&Adicionar Nvel por Categoria...Add Category &Level...treelocalcontrolNAdicionar sub ramo ao ramo seleccionado Add new child to selected parenttreelocalcontroltAdicionar ou modificar uma ligao externa para a internet!Add or modify an extrnal web linktreelocalcontroltAdicionar ou modificar uma ligao interna para outro ramo#Add or modify an internal node linktreelocalcontrolVNo possvel expandir sem campos em comum#Cannot expand without common fieldstreelocalcontrol(Campos de CategoriasCategory Fieldstreelocalcontrol$Limpar &FormataoClear For&mattingtreelocalcontrolhRemover a formatao do texto actual ou seleccionado)Clear current or selected text formattingtreelocalcontrolJColapsar descendentes juntando campos&Collapse descendants by merging fieldstreelocalcontrolDCopiar Tipos de Outro &Ficheiro...Copy Types from &File...treelocalcontrolbCopiar o ramo ou texto para a memria de trabalho(Copy the branch or text to the clipboardtreelocalcontrol\Copiar configurao de outro ficheiro TreeLine1Copy the configuration from another TreeLine filetreelocalcontrolCor&tarCu&ttreelocalcontrolbCortar o ramo ou texto para a memria de trabalho'Cut the branch or text to the clipboardtreelocalcontrol NormalDefaulttreelocalcontrol>Eliminar os ramos seleccionadosDelete the selected nodestreelocalcontrolhErro - no foi possvel apagar cpia de segurana {}'Error - could not delete backup file {}treelocalcontrolTErro - no foi possvel ler o ficheiro {0}Error - could not read file {0}treelocalcontrol`Erro - no foi possvel escrever para o ficheiroError - could not write to filetreelocalcontrolLErro - no possvel escrever para {}Error - could not write to {}treelocalcontrol^Exportar o ficheiro em diversos outros formatos(Export the file in various other formatstreelocalcontrolhExportar para PDF com as actuais opes de impresso+Export to PDF with current printing optionstreelocalcontrol Ficheiro gravado File savedtreelocalcontrol,&Nivelar por categoriaFlatten &by Categorytreelocalcontrol Co&r do Texto...Font C&olor...treelocalcontrol<Avanar os ramos seleccionadosIndent the selected nodestreelocalcontrol*Inserir Irmo &DepoisInsert Sibling &Aftertreelocalcontrol(Inserir Irmo &AntesInsert Sibling &Beforetreelocalcontrol~Insere ramos sobre descendentes classificanado segundo um campo$Insert category nodes above childrentreelocalcontrolHInserir novo ramo abaixo da seleco"Insert new sibling after selectiontreelocalcontrolFInserir novo ramo acima da seleco#Insert new sibling before selectiontreelocalcontrol&Ligao &Interna...Internal &Link...treelocalcontrol GrandeLargetreelocalcontrol MaiorLargertreelocalcontrolMuito GrandeLargesttreelocalcontrol"Mover para &Baixo M&ove Downtreelocalcontrol(Colocar em &Primeiro Move &Firsttreelocalcontrol$Colocar em &ltimo Move &LasttreelocalcontrolNMover para baixo os ramos seleccionadosMove the selected nodes downtreelocalcontrolMover os ramos seleccionados para que sejam os primeiros descendentes0Move the selected nodes to be the first childrentreelocalcontrolMover os ramos seleccionados para que sejam os ltimos descendentes/Move the selected nodes to be the last childrentreelocalcontrolLMover para cima os ramos seleccionadosMove the selected nodes uptreelocalcontrolDAbrir nova vista do mesmo ficheiro#Open a new window for the same filetreelocalcontrol6&Definies de Impresso...P&rint Setup...treelocalcontrolVColar ramos ou texto da memria de trabalho&Paste nodes or text from the clipboardtreelocalcontrolbColar texto sem formatao da memria de trabalho+Paste non-formatted text from the clipboardtreelocalcontrol*Imprimir para &PDF...Print &to PDF...treelocalcontrol8Pr &visualizar impresso...Print Pre&view...treelocalcontroltImprimir apresentao da rvore baseada nas opes actuais*Print tree output based on current optionstreelocalcontrol &Propriedades...Prop&erties...treelocalcontrol:Repe a ltima aco desfeitaRedo the previous undotreelocalcontrolTAlterar o nome da entrada actual na rvore#Rename the current tree entry titletreelocalcontrol Guardar &Como... Save &As...treelocalcontrol Guardar Ficheiro Save Filetreelocalcontrol2Guardar alteraes em {}?Save changes to {}?treelocalcontrol&Guardar alteraes? Save changes?treelocalcontrol2Guardar o ficheiro actualSave the current filetreelocalcontrolFGuardar o ficheiro com um novo nomeSave the file with a new nametreelocalcontrol@Seleccione campo para novo nvelSelect fields for new leveltreelocalcontrol0Definir tamanho do texto Set Font Sizetreelocalcontrol(Definir Tipo de Ramo Set Node TypetreelocalcontrolxDefinir parmetros do ficheiro como compresso e encriptao3Set file parameters like compression and encryptiontreelocalcontrol|Definir margens, tamanho de papel e outras opes de impresso1Set margins, page size and other printing optionstreelocalcontrolbDefinir o tamanho do texto actual ou seleccionado(Set size of the current or selected texttreelocalcontrolZDefinir a cor do texto actual ou seleccionado-Set the color of the current or selected texttreelocalcontrolfDefinir o texto actual ou seleccionado como negrito(Set the current or selected font to boldtreelocalcontrolfDefinir o texto actual ou seleccionado como itlico*Set the current or selected font to italictreelocalcontrollDefinir o texto actual ou seleccionado como sublinhado-Set the current or selected font to underlinetreelocalcontrolfDefinir o tipo de dados para os ramos seleccionados$Set the node type for selected nodestreelocalcontrollMostrar uma pr visualizao do resultado da impresso"Show a preview of printing resultstreelocalcontrolPequenoSmalltreelocalcontrolRTreeLine - Abrir Ficheiro de Configurao"TreeLine - Open Configuration Filetreelocalcontrol.TreeLine - Guardar ComoTreeLine - Save Astreelocalcontrol&SublinhadoU&nderline Fonttreelocalcontrol,Reverte a ltima acoUndo the previous actiontreelocalcontrol:Recuar os ramos seleccionadosUnindent the selected nodestreelocalcontrol,&Acerca do TreeLine...&About TreeLine...treemaincontrol*Utilizao &Bsica...&Basic Usage...treemaincontrol<&Cancelar Abertura de Ficheiro&Cancel File Opentreemaincontrol.&Procura Condicional...&Conditional Find...treemaincontrol:&Configurar Tipos de Dados...&Configure Data Types...treemaincontrol4&Apagar Cpia de Segurana&Delete Backuptreemaincontrol&&Encontrar Texto... &Find Text...treemaincontrol2&Documentao Completa...&Full Documentation...treemaincontrol"&Opes Gerais...&General Options...treemaincontrol&Importar... &Import...treemaincontrol&Novo...&New...treemaincontrol&Abrir...&Open...treemaincontrol &Sair&Quittreemaincontrol:&Restaurar Cpia de Segurana&Restore Backuptreemaincontrol"&Seleccionar Tudo &Select Alltreemaincontrol,&Seleccione um exemplo&Select Sampletreemaincontrol&Seleccionar &Modelo&Select Templatetreemaincontrol&Filtro de &Texto...&Text Filter...treemaincontrolCpia de segurana {0} existente. Uma sesso anterior pode ter terminado inesperadamente+O+O8#H4HJĠK LD)L}PSlZrB[`;[`\kU__1?8E,p0v% O%G400:0y0|00X55 D= DKn+L,>,ֽFH5 4H5=H5KH5f pf1f;fIf|Hfflb<>L `Ҟ`_A2 ege>DeLWįįޓ y,*y*yo*y*TL*0'*0+Fy+F+f+fC+z0+d+p0+R+z0U+u+8+Ct+L+y+Z+į+įpc+įۈ+0F0iG8Hw9Hw99"I.4IJ+J6xJ61J69J6>nJ6yJ6{J6VJ6KGeLZPLL1Lb !O|1RPFEPFE{PFEuTQV1V1Vl VϞWWT?WT%X;XeX˙<]XY Zg8\]4\]4H\|^v7jv"fSIA)[;IHyYɵn2@ɵn^ɵneɵnqɵnɵnUɵn) W B.RMElPqH-`<p5#QL%UT*42CCCe#D"uD1QMaR?CfP loR+Xw^}a|{yW2{2*.?*l dyvjurgR "l)*-T/=Ni1$<5~P< k?N{Nky]`4`  G)i 676x6^TB3p=}F,EE(@j{.8AARb[yLns4ukMxMEOREX&ww!e)*/eN5^;ByoOZfH`xcփu(% h$$*.(ԍ^ n,a;yF&H`m/_IxS.YMOYMXuh^ i%ssc~wOۊ1NVz]]MIII5I9ZI[II&IIYɉiy Iȉ  Iˉʉ IIuDS*uDZcoJ,,p,,,cxɘeŤ5$fR afRINƛIc#PqUV:Vz ǙJ .h*$%C ?"sKN;MKR~]]]dky^f{yG%Mصǥѧ+t{yr %.eC-q5|ƨyƨ˾e]ҝz է?f~bI5~bKo !c++3/!/V6 kGNLAUٿPѧ9Ui}UZÖZZZ>^ne iSiZy;r{}uk}w}w}w!jtMt.4.PDt([tYt_ Fv ʢ5oʢd6bdvwdd59b)ؘUBw0mu b2?6TCU]DK1U|'arut}wZ}$}$Շ}$ZK<;- /YEMquTi~5kEXU HbDbGlgAi$ax1 `z*2dsU5^z@>mNnДb*C<ʴ5yPʴ5ԄD)dF5F5zgYI $IHmAsp 5 }$w qeWc ڤ Et E AcP AcJk 35q K!?E@ bb b` b` i3% la|p lf |U t tJ. m @t (  > o f u K %'_   )u */ 7u =M B2 T^‡ ] `^g ` c( dי e eH f1Q& gn k, rD": x ~dQ $Q 9 I'S I-) I7 ; v  Je %p* ,y ,B +! ˔L P' PX Q Yb 68 :/ f  f D 4a s sF AAUE :& m, #-tw 0N+ E9 L' L Mc\=! Sc Vh ]$3 f) f)D io>O m` A w~ H7 HC3 $E .@Ӫ  iB / p ܖ J JG/ t.4 kڙ Ӈ k ̺M< -DNU k k U)[ < 0F  R  Z α xH] ./F 7FD >U >V >W >]6 >j5 >o >b > DT I. RVJ RV RV S.@ S Y [y j7oB@ p/ . Bfh ; T2 TrQ T T i g 4 H Sp )d, C .3 ._c .sY . a yS | U t :b[u ʜ- +>0 0E ;ɾ Ptz Pt9 feE fe g iFC iH if u2 w w w> w}O w}: w} Pm X ^| }nS R6 X - D? t5z( t5 {L  )J T)gT**(%*/E)c/Em=BkI_[XRu[ a.:dvɅYy$~\SiB&cݖ[yrE  F^"#n$U%4=o%4K-v 0i)0͎1c92wT~D!5HWJd\ZL$.c5ic5iCyC" {~a` :FNAYkyPt2,q@A[ikSobre o %1About %1MAC_APPLICATION_MENUOcultar %1Hide %1MAC_APPLICATION_MENUOcultar Outros Hide OthersMAC_APPLICATION_MENUPreferncias &Preferences...MAC_APPLICATION_MENUEncerrar %1Quit %1MAC_APPLICATION_MENUServiosServicesMAC_APPLICATION_MENUMostrar TudoShow AllMAC_APPLICATION_MENU"%1, %2 indefinido%1, %2 not definedQ3Accel,%1 ambguo no tratadoAmbiguous %1 not handledQ3AccelRemoverDelete Q3DataTable FalsoFalse Q3DataTableInserirInsert Q3DataTableVerdadeiroTrue Q3DataTableActualizarUpdate Q3DataTablez%1 Ficheiro no encontrado. Verifique a localizao e o nome.+%1 File not found. Check path and filename. Q3FileDialog&Apagar&Delete Q3FileDialog&No&No Q3FileDialog&OK&OK Q3FileDialog &Abrir&Open Q3FileDialog&Mudar Nome&Rename Q3FileDialog&Gravar&Save Q3FileDialogNo &Ordenado &Unsorted Q3FileDialog&Sim&Yes Q3FileDialogJ<qt>Deseja mesmo apagar %1 "%2"?</qt>1Are you sure you wish to delete %1 "%2"? Q3FileDialog,Todos os Ficheiros (*) All Files (*) Q3FileDialog0Todos os Ficheiros (*.*)All Files (*.*) Q3FileDialogAtributos Attributes Q3FileDialog RecuarBack Q3FileDialogCancelarCancel Q3FileDialog6Copiar ou Mover um FicheiroCopy or Move a File Q3FileDialog Criar Nova PastaCreate New Folder Q3FileDialogDataDate Q3FileDialogApagar %1 Delete %1 Q3FileDialogVista Detalhada Detail View Q3FileDialog PastaDir Q3FileDialog Pastas Directories Q3FileDialog Pasta: Directory: Q3FileDialogErroError Q3FileDialogFicheiroFile Q3FileDialog$&Nome do Ficheiro: File &name: Q3FileDialog$&Tipo de Ficheiro: File &type: Q3FileDialogProcurar PastaFind Directory Q3FileDialogInacessvel Inaccessible Q3FileDialogVista Abreviada List View Q3FileDialogVer &em: Look &in: Q3FileDialogNomeName Q3FileDialogNova Pasta New Folder Q3FileDialogNova Pasta %1 New Folder %1 Q3FileDialogNova Pasta 1 New Folder 1 Q3FileDialogPasta MeOne directory up Q3FileDialog AbrirOpen Q3FileDialog Abrir Open  Q3FileDialog8Antever Contedo do FicheiroPreview File Contents Q3FileDialog<Antever Informao do FicheiroPreview File Info Q3FileDialog&RecarregarR&eload Q3FileDialogApenas Leitura Read-only Q3FileDialog"Leitura e escrita Read-write Q3FileDialogLer: %1Read: %1 Q3FileDialogGuardar ComoSave As Q3FileDialog(Seleccione uma PastaSelect a Directory Q3FileDialog:Mostrar ficheiros &escondidosShow &hidden files Q3FileDialogTamanhoSize Q3FileDialogOrdenarSort Q3FileDialog$Ordenar pela &Data Sort by &Date Q3FileDialog$Ordenar pelo &Nome Sort by &Name Q3FileDialog*Ordenar pelo &Tamanho Sort by &Size Q3FileDialogEspecialSpecial Q3FileDialog$Ligao para PastaSymlink to Directory Q3FileDialog*Ligao para FicheiroSymlink to File Q3FileDialog*Ligao para EspecialSymlink to Special Q3FileDialogTipoType Q3FileDialogApenas Escrita Write-only Q3FileDialogEscrever: %1 Write: %1 Q3FileDialoga pasta the directory Q3FileDialogo ficheirothe file Q3FileDialoga ligao the symlink Q3FileDialogBNo foi possvel criar a pasta %1Could not create directory %1 Q3LocalFs2No foi possvel abrir %1Could not open %1 Q3LocalFs>No foi possvel ler a pasta %1Could not read directory %1 Q3LocalFs`No foi possvel apagar o ficheiro ou a pasta %1%Could not remove file or directory %1 Q3LocalFsRNo foi possvel mudar o nome %1 para %2Could not rename %1 to %2 Q3LocalFs8Nao foi possvel escrever %1Could not write %1 Q3LocalFsConfigurar... Customize... Q3MainWindowAlinharLine up Q3MainWindowJOperao interrompida pelo utilizadorOperation stopped by the userQ3NetworkProtocolCancelarCancelQ3ProgressDialogAplicarApply Q3TabDialogCancelarCancel Q3TabDialogPredefiniesDefaults Q3TabDialog AjudaHelp Q3TabDialogOKOK Q3TabDialog&Copiar&Copy Q3TextEdit Co&lar&Paste Q3TextEdit&Refazer&Redo Q3TextEdit&Desfazer&Undo Q3TextEdit LimparClear Q3TextEditCor&tarCu&t Q3TextEdit Seleccionar Tudo Select All Q3TextEdit FecharClose Q3TitleBarFecha a janelaCloses the window Q3TitleBarNContm comandos para manipular a janela*Contains commands to manipulate the window Q3TitleBarvMostra o nome da janela e contm controlos para a manipularFDisplays the name of the window and contains controls to manipulate it Q3TitleBar@Coloca a janela em ecr completoMakes the window full screen Q3TitleBarMaximizarMaximize Q3TitleBarMinimizarMinimize Q3TitleBar.Tira a janela da frenteMoves the window out of the way Q3TitleBarZColoca uma janela maximizada no estado normal&Puts a maximized window back to normal Q3TitleBarZColoca uma janela minimizada no estado normalPuts a minimized back to normal Q3TitleBar Restaurar abaixo Restore down Q3TitleBarRestaurar acima Restore up Q3TitleBarSistemaSystem Q3TitleBarMais...More... Q3ToolBar(desconhecido) (unknown) Q3UrlOperatorO protocolo '%1' no suporta copiar ou mover ficheiros ou pastasIThe protocol `%1' does not support copying or moving files or directories Q3UrlOperatorhO protocolo '%1' no suporta criao de novas pastas;The protocol `%1' does not support creating new directories Q3UrlOperatordO protocolo '%1' no suporta obteno de ficheiros0The protocol `%1' does not support getting files Q3UrlOperator^O protocolo '%1' no suporta listagem de pastas6The protocol `%1' does not support listing directories Q3UrlOperatorfO protocolo '%1' no suporta colocao de ficheiros0The protocol `%1' does not support putting files Q3UrlOperator|O protocolo '%1' no suporta eliminao de ficheiros ou pastas@The protocol `%1' does not support removing files or directories Q3UrlOperatorO protocolo '%1' no suporta mudana de nome de ficheiros ou pastas@The protocol `%1' does not support renaming files or directories Q3UrlOperator@O protocolo '%1' no suportado"The protocol `%1' is not supported Q3UrlOperator&Cancelar&CancelQ3Wizard&Terminar&FinishQ3Wizard &Ajuda&HelpQ3Wizard&Avanar >&Next >Q3Wizard< &Recuar< &BackQ3Wizard Ligao recusadaConnection refusedQAbstractSocket Ligao expiradaConnection timed outQAbstractSocket(Mquina desconhecidaHost not foundQAbstractSocket"Rede inalcanvelNetwork unreachableQAbstractSocket$'Socket' desligadoSocket is not connectedQAbstractSocket:Operao de 'socket' expiradaSocket operation timed outQAbstractSocket&Passo acima&Step upQAbstractSpinBoxPasso &abaixo Step &downQAbstractSpinBoxActivarActivate QApplicationJActiva a janela principal do programa#Activates the program's main window QApplicationdO executvel '%1' requere Qt %2, Qt %3 encontrado.,Executable '%1' requires Qt %2, found Qt %3. QApplicationTErro de Incompatibilidade da Biblioteca QtIncompatible Qt Library Error QApplication&Cancelar&Cancel QAxSelect&Objecto COM: COM &Object: QAxSelectOKOK QAxSelect8Seleccionar Controlo ActiveXSelect ActiveX Control QAxSelectActivarCheck QCheckBoxComutarToggle QCheckBoxDesactivarUncheck QCheckBox@&Adicionar s Cores Customizadas&Add to Custom Colors QColorDialogCores &bsicas &Basic colors QColorDialog&Cores c&ustomizadas&Custom colors QColorDialogV&erde:&Green: QColorDialog&Vermelho:&Red: QColorDialog&Saturao:&Sat: QColorDialog&Valor:&Val: QColorDialog*Canal &transparncia:A&lpha channel: QColorDialog &Azul:Bl&ue: QColorDialog C&or:Hu&e: QColorDialog FecharClose QComboBox FalsoFalse QComboBox AbrirOpen QComboBoxVerdadeiroTrue QComboBoxLFinalizao de transaco no possvelUnable to commit transaction QDB2Driver(Ligao no possvelUnable to connect QDB2DriverFAnulao de transaco no possvelUnable to rollback transaction QDB2DriverFFinalizao automtica no possvelUnable to set autocommit QDB2Driver@Ligao de varivel no possvelUnable to bind variable QDB2Result*Execuo no possvelUnable to execute statement QDB2ResultBObteno do primeiro no possvelUnable to fetch first QDB2ResultBObteno do seguinte no possvelUnable to fetch next QDB2ResultFObteno do registo %1 no possvelUnable to fetch record %1 QDB2Result.Preparao no possvelUnable to prepare statement QDB2ResultAMAM QDateTimeEditPMPM QDateTimeEditamam QDateTimeEditpmpm QDateTimeEditO Que Isto? What's This?QDialog&Cancelar&CancelQDialogButtonBox&Fechar&CloseQDialogButtonBox&No&NoQDialogButtonBox&OK&OKQDialogButtonBox&Gravar&SaveQDialogButtonBox&Sim&YesQDialogButtonBoxAbortarAbortQDialogButtonBoxAplicarApplyQDialogButtonBoxCancelarCancelQDialogButtonBox FecharCloseQDialogButtonBox"Fechar sem GravarClose without SavingQDialogButtonBoxDescartarDiscardQDialogButtonBoxNo Gravar Don't SaveQDialogButtonBox AjudaHelpQDialogButtonBoxIgnorarIgnoreQDialogButtonBoxN&o para Todos N&o to AllQDialogButtonBoxOKOKQDialogButtonBox AbrirOpenQDialogButtonBoxRestaurarResetQDialogButtonBox.Restaurar PredefiniesRestore DefaultsQDialogButtonBox Tentar NovamenteRetryQDialogButtonBox GravarSaveQDialogButtonBoxGravar TodosSave AllQDialogButtonBoxSim para &Todos Yes to &AllQDialogButtonBox&Data de Modificao Date Modified QDirModelTipoKind QDirModelNomeName QDirModelTamanhoSize QDirModelTipoType QDirModel FecharClose QDockWidget MenosLessQDoubleSpinBoxMaisMoreQDoubleSpinBox&OK&OK QErrorMessage@&Mostrar esta mensagem novamente&Show this message again QErrorMessage&Mensagem Depurao:Debug Message: QErrorMessageErro Fatal: Fatal Error: QErrorMessage Aviso:Warning: QErrorMessagez%1 Pasta no encontrada. Por favor verifique o nome da pasta.K%1 Directory not found. Please verify the correct directory name was given. QFileDialog%1 Ficheiro no encontrado. Por favor verifique o nome do ficheiro.A%1 File not found. Please verify the correct file name was given. QFileDialog@%1 j existe. Deseja substituir?-%1 already exists. Do you want to replace it? QFileDialog&Apagar&Delete QFileDialog &Abrir&Open QFileDialog&Mudar o Nome&Rename QFileDialog&Gravar&Save QFileDialog'%1' est protegido contra escrita. Deseja apagar de qualquer forma?9'%1' is write protected. Do you want to delete it anyway? QFileDialog,Todos os Ficheiros (*) All Files (*) QFileDialog0Todos os Ficheiros (*.*)All Files (*.*) QFileDialog2Deseja mesmo apagar '%1'?!Are sure you want to delete '%1'? QFileDialog RecuarBack QFileDialog@No foi possvel apagar a pasta.Could not delete directory. QFileDialog Criar Nova PastaCreate New Folder QFileDialogVista Detalhada Detail View QFileDialog Pastas Directories QFileDialog Pasta: Directory: QFileDialogUnidadeDrive QFileDialogFicheiroFile QFileDialog$&Nome do Ficheiro: File &name: QFileDialog$FIcheiros do tipo:Files of type: QFileDialogProcurar PastaFind Directory QFileDialogSeguinteForward QFileDialogVista Abreviada List View QFileDialog O Meu Computador My Computer QFileDialogNova Pasta New Folder QFileDialog AbrirOpen QFileDialogPasta MeParent Directory QFileDialogGravar ComoSave As QFileDialog:Mostrar ficheiros &escondidosShow &hidden files QFileDialogDesconhecidoUnknown QFileDialog&Data de Modificao Date ModifiedQFileSystemModelTipoKindQFileSystemModel O Meu Computador My ComputerQFileSystemModelNomeNameQFileSystemModelTamanhoSizeQFileSystemModelTipoTypeQFileSystemModel&Tipo de Letra&Font QFontDialog&Tamanho&Size QFontDialog&Sublinhar &Underline QFontDialogEfeitosEffects QFontDialog*&Estilo Tipo de Letra Font st&yle QFontDialogAmostraSample QFontDialog0Seleccione Tipo de Letra Select Font QFontDialog&Riscar Stri&keout QFontDialog&&Sistema de EscritaWr&iting System QFontDialog:A mudana de pasta falhou: %1Changing directory failed: %1QFtp$Ligado ao servidorConnected to hostQFtp*Ligado ao servidor %1Connected to host %1QFtp@A ligao ao servidor falhou: %1Connecting to host failed: %1QFtpLigao fechadaConnection closedQFtp2Ligao de dados recusada&Connection refused for data connectionQFtp>Ligao ao servidor %1 recusadaConnection refused to host %1QFtp(Ligao a %1 fechadaConnection to %1 closedQFtp:A criao da pasta falhou: %1Creating directory failed: %1QFtpBA descarga do ficheiro falhou: %1Downloading file failed: %1QFtp,Servidor %1 encontrado Host %1 foundQFtp4Servidor %1 no encontradoHost %1 not foundQFtp&Servidor encontrado Host foundQFtp<A listagem da pasta falhou: %1Listing directory failed: %1QFtp2A autenticao falhou: %1Login failed: %1QFtpDesligado Not connectedQFtp:A remoo da pasta falhou: %1Removing directory failed: %1QFtp@A remoo do ficheiro falhou: %1Removing file failed: %1QFtp"Erro desconhecido Unknown errorQFtpJO carregamento do ficheiro falhou: %1Uploading file failed: %1QFtpLTRQT_LAYOUT_DIRECTIONQGuiApplication"Erro desconhecido Unknown error QHostInfo.Servidor No encontradoHost not foundQHostInfoAgent:Tipo de endereo desconhecidoUnknown address typeQHostInfoAgent"Erro desconhecido Unknown errorQHostInfoAgent$Ligado ao servidorConnected to hostQHttp*Ligado ao servidor %1Connected to host %1QHttpLigao fechadaConnection closedQHttp Ligao recusadaConnection refusedQHttp(Ligao a %1 fechadaConnection to %1 closedQHttp(O pedido HTTP falhouHTTP request failedQHttp,Servidor %1 encontrado Host %1 foundQHttp4Servidor %1 no encontradoHost %1 not foundQHttp&Servidor encontrado Host foundQHttp6Corpo parcial HTTP invlidoInvalid HTTP chunked bodyQHttpFCabealho de resposta HTTP invlidoInvalid HTTP response headerQHttp4Nenhum servidor para ligarNo server set to connect toQHttpPedido abortadoRequest abortedQHttpVO servidor fechou a ligao inesperadamente%Server closed connection unexpectedlyQHttp"Erro desconhecido Unknown errorQHttp4Tamanho de contedo erradoWrong content lengthQHttpJNo foi possvel iniciar a transacoCould not start transaction QIBaseDriver:Erro ao abrir a base de dadosError opening database QIBaseDriverNNo foi possvel finalizar a transacoUnable to commit transaction QIBaseDriverHNo foi possvel anular a transacoUnable to rollback transaction QIBaseDriverFNo foi possvel alocar a expressoCould not allocate statement QIBaseResultbNo foi possvel descrever a expresso de entrada"Could not describe input statement QIBaseResultLNo foi possvel descrever a expressoCould not describe statement QIBaseResultTNo foi possvel obter o elemento seguinteCould not fetch next item QIBaseResultDNo foi possvel encontrar o arrayCould not find array QIBaseResultPNo foi possvel obter os dados do arrayCould not get array data QIBaseResultTNo foi possvel obter informao da queryCould not get query info QIBaseResult\No foi possvel obter informao da expressoCould not get statement info QIBaseResultJNo foi possvel preparar a expressoCould not prepare statement QIBaseResultJNo foi possvel iniciar a transacoCould not start transaction QIBaseResultFNo foi possvel fechar a expressoUnable to close statement QIBaseResultNNo foi possvel finalizar a transacoUnable to commit transaction QIBaseResult:No foi possvel criar o BLOBUnable to create BLOB QIBaseResultBNo foi possvel executar a queryUnable to execute query QIBaseResult:No foi possvel abrir o BLOBUnable to open BLOB QIBaseResult6No foi possvel ler o BLOBUnable to read BLOB QIBaseResult@No foi possvel escrever o BLOBUnable to write BLOB QIBaseResult8Dispositivo sem espao livreNo space left on device QIODevice:Ficheiro ou pasta inexistenteNo such file or directory QIODevice Permisso negadaPermission denied QIODevice8Demasiados ficheiros abertosToo many open files QIODevice"Erro desconhecido Unknown error QIODevice4Mtodo de entrada Max OS XMac OS X input method QInputContext2Mtodo de entrada WindowsWindows input method QInputContextXIMXIM QInputContext*Mtodo de entrada XIMXIM input method QInputContext@No foi possivel mapear '%1': %2Could not mmap '%1': %2QLibraryFNo foi possvel desmapear '%1': %2Could not unmap '%1': %2QLibrarydDados de verificao do plugin incorrectos em '%1')Plugin verification data mismatch in '%1'QLibraryO plugin '%1' usa uma biblioteca Qt incompatvel. (%2.%3.%4) [%5]=The plugin '%1' uses incompatible Qt library. (%2.%3.%4) [%5]QLibraryO plugin '%1' usa uma biblioteca Qt incompatvel. A chave de compilao esperada "%2", ficou "%3"OThe plugin '%1' uses incompatible Qt library. Expected build key "%2", got "%3"QLibrary"Erro desconhecido Unknown errorQLibrary&Copiar&Copy QLineEdit Co&lar&Paste QLineEdit&Refazer&Redo QLineEdit&Desfazer&Undo QLineEditCor&tarCu&t QLineEdit ApagarDelete QLineEdit Seleccionar Tudo Select All QLineEditJNo foi possvel iniciar a transacoUnable to begin transaction QMYSQLDriverNNo foi possvel finalizar a transacoUnable to commit transaction QMYSQLDriverLNo foi possvel estabelecer a ligaoUnable to connect QMYSQLDriverPNo foi possvel abrir a base de dados 'Unable to open database ' QMYSQLDriverHNo foi possvel anular a transacoUnable to rollback transaction QMYSQLDriverjNo foi possvel fazer a ligao dos valores externosUnable to bind outvalues QMYSQLResultRNo foi possvel fazer a ligao do valorUnable to bind value QMYSQLResultBNo foi possvel executar a queryUnable to execute query QMYSQLResultJNo foi possvel executar a expressoUnable to execute statement QMYSQLResult8No foi possvel obter dadosUnable to fetch data QMYSQLResultJNo foi possvel preparar a expressoUnable to prepare statement QMYSQLResultLNo foi possvel restaurar a expressoUnable to reset statement QMYSQLResultHNo foi possvel guardar o resultadoUnable to store result QMYSQLResultfNo foi possvel guardar os resultados da expresso!Unable to store statement results QMYSQLResult%1 - [%2] %1 - [%2] QMdiSubWindow&Fechar&Close QMdiSubWindow &Mover&Move QMdiSubWindow&Restaurar&Restore QMdiSubWindow&Tamanho&Size QMdiSubWindow FecharClose QMdiSubWindow AjudaHelp QMdiSubWindowMa&ximizar Ma&ximize QMdiSubWindowMaximizarMaximize QMdiSubWindowMenuMenu QMdiSubWindowMi&nimizar Mi&nimize QMdiSubWindowMinimizarMinimize QMdiSubWindowRestaurar Baixo Restore Down QMdiSubWindow&Permanecer no &Topo Stay on &Top QMdiSubWindow FecharCloseQMenuExecutarExecuteQMenu AbrirOpenQMenuAcerca do QtAbout Qt QMessageBox AjudaHelp QMessageBox.No Mostrar Detalhes...Hide Details... QMessageBoxOKOK QMessageBox&Mostrar Detalhes...Show Details... QMessageBox8Seleccione Mtodo de Entrada Select IMQMultiInputContextDSeleccionador de mtodo de entradaMultiple input method switcherQMultiInputContextPluginSeleccionador de mtodo de entrada que utiliza o menu de contexto dos elementos de textoMMultiple input method switcher that uses the context menu of the text widgetsQMultiInputContextPlugin\Outro 'socket' j est escuta no mesmo porto4Another socket is already listening on the same portQNativeSocketEngineTentativa de utilizao de 'socket' IPv6 numa plataforma sem suporte IPv6=Attempt to use IPv6 socket on a platform with no IPv6 supportQNativeSocketEngine Ligao recusadaConnection refusedQNativeSocketEngine Ligao expiradaConnection timed outQNativeSocketEngineLDatagrama demasiado grande para enviarDatagram was too large to sendQNativeSocketEngine(Mquina inalcanvelHost unreachableQNativeSocketEngine<Descritor de 'socket' invlidoInvalid socket descriptorQNativeSocketEngineErro de rede Network errorQNativeSocketEngine2Operao de rede expiradaNetwork operation timed outQNativeSocketEngine"Rede inalcanvelNetwork unreachableQNativeSocketEngine0Operao em no 'socket'Operation on non-socketQNativeSocketEngineSem recursosOut of resourcesQNativeSocketEngine Permisso negadaPermission deniedQNativeSocketEngine>Tipo de protocolo no suportadoProtocol type not supportedQNativeSocketEngine<O endereo no est disponvelThe address is not availableQNativeSocketEngine2O endereo est protegidoThe address is protectedQNativeSocketEngineHO endereo de ligao j est em uso#The bound address is already in useQNativeSocketEngineBA mquina remota fechou a ligao%The remote host closed the connectionQNativeSocketEnginehNo foi possvel inicializar 'socket' de transmisso%Unable to initialize broadcast socketQNativeSocketEnginehNo foi possvel inicializar 'socket' no bloqueante(Unable to initialize non-blocking socketQNativeSocketEngineJNo foi possvel receber uma mensagemUnable to receive a messageQNativeSocketEngineHNo foi possvel enviar uma mensagemUnable to send a messageQNativeSocketEngine2No foi possvel escreverUnable to writeQNativeSocketEngine"Erro desconhecido Unknown errorQNativeSocketEngineDOperao de 'socket' no suportadaUnsupported socket operationQNativeSocketEngineJNo foi possvel iniciar a transacoUnable to begin transaction QOCIDriver8No foi possvel inicializarUnable to initialize QOCIDriver6No foi possvel autenticarUnable to logon QOCIDriverFNo foi possvel alocar a expressoUnable to alloc statement QOCIResultNo foi possvel fazer a licao da coluna para execuo 'batch''Unable to bind column for batch execute QOCIResultVNo foi possvel fazer o ligamento do valorUnable to bind value QOCIResult`No foi possvel executar a expresso de 'batch'!Unable to execute batch statement QOCIResultJNo foi possvel executar a expressoUnable to execute statement QOCIResultFNo foi possvel passar ao seguinteUnable to goto next QOCIResultJNo foi possvel preparar a expressoUnable to prepare statement QOCIResultNNo foi possvel finalizar a transacoUnable to commit transaction QODBCDriver,No foi possvel ligarUnable to connect QODBCDriverNo foi possvel ligar - O 'driver' no suporta todas as funcionalidades necessriasCUnable to connect - Driver doesn't support all needed functionality QODBCDriverfNo foi possvel desactivar finalizao automticaUnable to disable autocommit QODBCDriver^No foi possvel activar finalizao automticaUnable to enable autocommit QODBCDriverHNo foi possvel anular a transacoUnable to rollback transaction QODBCDriver(QODBCResult::reset: No foi possvel definir 'SQL_CURSOR_STATIC' como atributo da expresso. Por favor verifique a configurao do seu 'driver' ODBCyQODBCResult::reset: Unable to set 'SQL_CURSOR_STATIC' as statement attribute. Please check your ODBC driver configuration QODBCResult\No foi possvel fazer o ligamento da varivelUnable to bind variable QODBCResultJNo foi possvel executar a expressoUnable to execute statement QODBCResultBObteno do primeiro no possvelUnable to fetch first QODBCResultBNo foi possvel obter o seguinteUnable to fetch next QODBCResultJNo foi possvel preparar a expressoUnable to prepare statement QODBCResult IncioHomeQObjectNomeNameQPPDOptionsModel ValorValueQPPDOptionsModelJNo foi possvel iniciar a transacoCould not begin transaction QPSQLDriverNNo foi possvel finalizar a transacoCould not commit transaction QPSQLDriverHNo foi possvel anular a transacoCould not rollback transaction QPSQLDriver,No foi possvel ligarUnable to connect QPSQLDriver@No foi possvel criar a 'query'Unable to create query QPSQLResultPaisagem LandscapeQPageSetupWidgetTamanho pgina: Page size:QPageSetupWidgetFonte papel: Paper source:QPageSetupWidgetRetratoPortraitQPageSetupWidget"Erro desconhecido Unknown error QPluginLoader@%1 j existe. Deseja substituir?/%1 already exists. Do you want to overwrite it? QPrintDialog@<qt>Deseja gravar por cima?</qt>%Do you want to overwrite it? QPrintDialog$A0 (841 x 1189 mm)A0 (841 x 1189 mm) QPrintDialog"A1 (594 x 841 mm)A1 (594 x 841 mm) QPrintDialog"A2 (420 x 594 mm)A2 (420 x 594 mm) QPrintDialog"A3 (297 x 420 mm)A3 (297 x 420 mm) QPrintDialogPA4 (210 x 297 mm, 8.26 x 11.7 polegadas)%A4 (210 x 297 mm, 8.26 x 11.7 inches) QPrintDialog"A5 (148 x 210 mm)A5 (148 x 210 mm) QPrintDialog"A6 (105 x 148 mm)A6 (105 x 148 mm) QPrintDialog A7 (74 x 105 mm)A7 (74 x 105 mm) QPrintDialogA8 (52 x 74 mm)A8 (52 x 74 mm) QPrintDialogA9 (37 x 52 mm)A9 (37 x 52 mm) QPrintDialog,Nomes Alternativos: %1 Aliases: %1 QPrintDialog&B0 (1000 x 1414 mm)B0 (1000 x 1414 mm) QPrintDialog$B1 (707 x 1000 mm)B1 (707 x 1000 mm) QPrintDialog B10 (31 x 44 mm)B10 (31 x 44 mm) QPrintDialog"B2 (500 x 707 mm)B2 (500 x 707 mm) QPrintDialog"B3 (353 x 500 mm)B3 (353 x 500 mm) QPrintDialog"B4 (250 x 353 mm)B4 (250 x 353 mm) QPrintDialogPB5 (176 x 250 mm, 6.93 x 9.84 polegadas)%B5 (176 x 250 mm, 6.93 x 9.84 inches) QPrintDialog"B6 (125 x 176 mm)B6 (125 x 176 mm) QPrintDialog B7 (88 x 125 mm)B7 (88 x 125 mm) QPrintDialogB8 (62 x 88 mm)B8 (62 x 88 mm) QPrintDialogB9 (44 x 62 mm)B9 (44 x 62 mm) QPrintDialog$C5E (163 x 229 mm)C5E (163 x 229 mm) QPrintDialog$DLE (110 x 220 mm)DLE (110 x 220 mm) QPrintDialogXExecutivo (7.5 x 10 polegadas, 191 x 254 mm))Executive (7.5 x 10 inches, 191 x 254 mm) QPrintDialogNo possvel escrever no ficheiro %1. Por favor escolha um nome diferente.=File %1 is not writable. Please choose a different file name. QPrintDialog"O ficheiro existe File exists QPrintDialog(Folio (210 x 330 mm)Folio (210 x 330 mm) QPrintDialog*Ledger (432 x 279 mm)Ledger (432 x 279 mm) QPrintDialogPLegal (8.5 x 14 polegadas, 216 x 356 mm)%Legal (8.5 x 14 inches, 216 x 356 mm) QPrintDialogPCarta (8.5 x 11 polegadas, 216 x 279 mm)&Letter (8.5 x 11 inches, 216 x 279 mm) QPrintDialogOKOK QPrintDialogImprimirPrint QPrintDialog4Imprimir Para Ficheiro ...Print To File ... QPrintDialogImprimir todas Print all QPrintDialog&Seleco de pginas Print range QPrintDialog*Seleco de ImpressoPrint selection QPrintDialog.Tablide (279 x 432 mm)Tabloid (279 x 432 mm) QPrintDialogJEnvelope #10 Comum EUA (105 x 241 mm)%US Common #10 Envelope (105 x 241 mm) QPrintDialog"ligado localmentelocally connected QPrintDialogdesconhecidounknown QPrintDialog FecharCloseQPrintPreviewDialogPaisagem LandscapeQPrintPreviewDialogRetratoPortraitQPrintPreviewDialog JuntarCollateQPrintSettingsOutput CpiasCopiesQPrintSettingsOutput OpesOptionsQPrintSettingsOutputPginas de Pages fromQPrintSettingsOutputImprimir todas Print allQPrintSettingsOutput&Seleco de pginas Print rangeQPrintSettingsOutputSeleco SelectionQPrintSettingsOutputatoQPrintSettingsOutputImpressoraPrinter QPrintWidgetCancelarCancelQProgressDialog AbrirOpen QPushButtonActivarCheck QRadioButtonDm sintaxe de classe de caracteresbad char class syntaxQRegExp2m sintaxe de antecipaobad lookahead syntaxQRegExp.m sintaxe de repetiobad repetition syntaxQRegExp^funcionalidade desactivada est a ser utilizadadisabled feature usedQRegExp(valor octal invlidoinvalid octal valueQRegExp0limite interno alcanadomet internal limitQRegExp:delimitador esquerdo em faltamissing left delimQRegExpsem errosno error occurredQRegExpfim inesperadounexpected endQRegExp6Erro ao abrir base de dadosError to open databaseQSQLite2DriverJNo foi possvel iniciar a transacoUnable to begin transactionQSQLite2DriverNNo foi possvel finalizar a transacoUnable to commit transactionQSQLite2DriverHNo foi possvel anular a transacoUnable to rollback TransactionQSQLite2DriverJNo foi possvel executar a expressoUnable to execute statementQSQLite2ResultHNo foi possvel obter os resultadosUnable to fetch resultsQSQLite2Result<Erro ao fechar a base de dadosError closing database QSQLiteDriver:Erro ao abrir a base de dadosError opening database QSQLiteDriverJNo foi possvel iniciar a transacoUnable to begin transaction QSQLiteDriverNNo foi possvel finalizar a transacoUnable to commit transaction QSQLiteDriverVIncorrespondncia de contagem de parmetrosParameter count mismatch QSQLiteResult^No foi possvel fazer a ligao dos parametrosUnable to bind parameters QSQLiteResultJNo foi possvel executar a expressoUnable to execute statement QSQLiteResult<No foi possvel obter a linhaUnable to fetch row QSQLiteResultLNo foi possvel restaurar a expressoUnable to reset statement QSQLiteResult FundoBottom QScrollBarBorda esquerda Left edge QScrollBarLinha abaixo Line down QScrollBarLinha acimaLine up QScrollBar"Pgina para baixo Page down QScrollBar(Pgina para esquerda Page left QScrollBar&Pgina para direita Page right QScrollBar Pgina para cimaPage up QScrollBarPosioPosition QScrollBarBorda direita Right edge QScrollBar&Deslizar para baixo Scroll down QScrollBarDeslizar aqui Scroll here QScrollBar,Deslizar para esquerda Scroll left QScrollBar.Deslizar para a direita Scroll right QScrollBar$Deslizar para cima Scroll up QScrollBarTopoTop QScrollBar++ QShortcutAltAlt QShortcutAnteriorBack QShortcutBackspace Backspace QShortcutBacktabBacktab QShortcutBass Boost Bass Boost QShortcutBass Baixo Bass Down QShortcutBass CimaBass Up QShortcut ChamarCall QShortcutCaps Lock Caps Lock QShortcutCapsLockCapsLock QShortcutContexto1Context1 QShortcutContexto2Context2 QShortcutContexto3Context3 QShortcutContexto4Context4 QShortcutCtrlCtrl QShortcut DeleteDel QShortcut DeleteDelete QShortcut BaixoDown QShortcutEndEnd QShortcut EnterEnter QShortcutEscEsc QShortcut EscapeEscape QShortcutF%1F%1 QShortcutFavoritos Favorites QShortcutInverterFlip QShortcutSeguinteForward QShortcutDesligarHangup QShortcut AjudaHelp QShortcutHomeHome QShortcut Pgina Principal Home Page QShortcut InsertIns QShortcut InsertInsert QShortcutExecutar (0) Launch (0) QShortcutExecutar (1) Launch (1) QShortcutExecutar (2) Launch (2) QShortcutExecutar (3) Launch (3) QShortcutExecutar (4) Launch (4) QShortcutExecutar (5) Launch (5) QShortcutExecutar (6) Launch (6) QShortcutExecutar (7) Launch (7) QShortcutExecutar (8) Launch (8) QShortcutExecutar (9) Launch (9) QShortcutExecutar (A) Launch (A) QShortcutExecutar (B) Launch (B) QShortcutExecutar (C) Launch (C) QShortcutExecutar (D) Launch (D) QShortcutExecutar (E) Launch (E) QShortcutExecutar (F) Launch (F) QShortcut&Correio Electrnico Launch Mail QShortcut Mdia Launch Media QShortcutEsquerdaLeft QShortcutMdia Seguinte Media Next QShortcutTocar Mdia Media Play QShortcutMdia AnteriorMedia Previous QShortcutGravao Mdia Media Record QShortcutParar Mdia Media Stop QShortcutMenuMenu QShortcutMetaMeta QShortcutNoNo QShortcutNum LockNum Lock QShortcutNum LockNumLock QShortcutNumber Lock Number Lock QShortcutAbrir EndereoOpen URL QShortcutPage Down Page Down QShortcutPage UpPage Up QShortcut PausePause QShortcut PgDownPgDown QShortcutPgUpPgUp QShortcut PrintPrint QShortcutPrint Screen Print Screen QShortcutRefrescarRefresh QShortcut ReturnReturn QShortcutDireitaRight QShortcutScroll Lock Scroll Lock QShortcutScrollLock ScrollLock QShortcutProcurarSearch QShortcut SelectSelect QShortcut ShiftShift QShortcut SpaceSpace QShortcutHibernaoStandby QShortcut PararStop QShortcut SysReqSysReq QShortcutSystem RequestSystem Request QShortcutTabTab QShortcutTreble Baixo Treble Down QShortcutTreble Cima Treble Up QShortcutCimaUp QShortcutVolume Cima Volume Down QShortcutVolume Mute Volume Mute QShortcutVolume Baixo Volume Up QShortcutSimYes QShortcut"Pgina para baixo Page downQSlider(Pgina para esquerda Page leftQSlider&Pgina para direita Page rightQSlider Pgina para cimaPage upQSliderPosioPositionQSlider2Operao de rede expiradaNetwork operation timed outQSocks5SocketEngine MenosLessQSpinBoxMaisMoreQSpinBoxCancelarCancelQSql.Cancelar as alteraes?Cancel your edits?QSqlConfirmarConfirmQSql ApagarDeleteQSql(Apagar este registo?Delete this record?QSqlInserirInsertQSqlNoNoQSql*Gravar as alteraes? Save edits?QSqlActualizarUpdateQSqlSimYesQSqlLNo foi possvel estabelecer a ligaoUnable to open connection QTDSDriverRNo foi possvel utilizar a base de dadosUnable to use database QTDSDriver,Deslizar para Esquerda Scroll LeftQTabBar*Deslizar para Direita Scroll RightQTabBar&Copiar&Copy QTextControl Co&lar&Paste QTextControl&Refazer&Redo QTextControl&Desfazer&Undo QTextControl<Copiar &Localizao da LigaoCopy &Link Location QTextControlCor&tarCu&t QTextControl ApagarDelete QTextControl Seleccionar Tudo Select All QTextControl AbrirOpen QToolButtonPressionarPress QToolButton@Esta plataforma no suporta IPv6#This platform does not support IPv6 QUdpSocketRefazerRedo QUndoGroupDesfazerUndo QUndoGroup<vazio> QUndoModelRefazerRedo QUndoStackDesfazerUndo QUndoStackHInserir carcter de controlo Unicode Insert Unicode control characterQUnicodeControlCharacterMenuVLRE Incio de encaixe esquerda-para-direita$LRE Start of left-to-right embeddingQUnicodeControlCharacterMenu>LRM Marca esquerda-para-direitaLRM Left-to-right markQUnicodeControlCharacterMenu`LRO Incio de sobreposio esquerda-para-direita#LRO Start of left-to-right overrideQUnicodeControlCharacterMenu<PDF Formatao pop direccionalPDF Pop directional formattingQUnicodeControlCharacterMenuVRLE Incio de encaixe direita-para-esquerda$RLE Start of right-to-left embeddingQUnicodeControlCharacterMenu>RLM Marca direita-para-esquerdaRLM Right-to-left markQUnicodeControlCharacterMenu`RLO Incio de sobreposio direita-para-esquerda#RLO Start of right-to-left overrideQUnicodeControlCharacterMenu>ZWJ Ligador de comprimento zeroZWJ Zero width joinerQUnicodeControlCharacterMenuHZWNJ No-ligador de comprimento zeroZWNJ Zero width non-joinerQUnicodeControlCharacterMenu>ZWSP Espao de comprimento zeroZWSP Zero width spaceQUnicodeControlCharacterMenu FundoBottomQWebPageIgnorarIgnoreQWebPageIgnorar Ignore Grammar context menu itemIgnoreQWebPageBorda esquerda Left edgeQWebPage"Pgina para baixo Page downQWebPage(Pgina para esquerda Page leftQWebPage&Pgina para direita Page rightQWebPage Pgina para cimaPage upQWebPageRestaurarResetQWebPageBorda direita Right edgeQWebPage&Deslizar para baixo Scroll downQWebPageDeslizar aqui Scroll hereQWebPage,Deslizar para esquerda Scroll leftQWebPage.Deslizar para a direita Scroll rightQWebPage$Deslizar para cima Scroll upQWebPage PararStopQWebPageTopoTopQWebPageDesconhecidoUnknownQWebPageO Que Isto? What's This?QWhatsThisAction**QWidget&Terminar&FinishQWizard &Ajuda&HelpQWizard&Avanar >&Next >QWizard< &Recuar< &BackQWizardCancelarCancelQWizard AjudaHelpQWizard%1 - [%2] %1 - [%2] QWorkspace&Fechar&Close QWorkspace &Mover&Move QWorkspace&Restaurar&Restore QWorkspace&Tamanho&Size QWorkspace&Sair Sombra&Unshade QWorkspace FecharClose QWorkspaceMa&ximizar Ma&ximize QWorkspaceMi&nimizar Mi&nimize QWorkspaceMinimizarMinimize QWorkspaceRestaurar Baixo Restore Down QWorkspaceSombr&aSh&ade QWorkspace&Permanecer no &Topo Stay on &Top QWorkspacedeclarao de codificao ou declarao nica esperada ao ler a declarao XMLYencoding declaration or standalone declaration expected while reading the XML declarationQXmlTerro na declarao de uma entidade externa3error in the text declaration of an external entityQXml6erro ao analisar comentrio$error occurred while parsing commentQXml6erro ao analisar o contedo$error occurred while parsing contentQXmlberro ao analisar a definio de tipo de documento5error occurred while parsing document type definitionQXml2erro ao analisar elemento$error occurred while parsing elementQXml6erro ao analisar referncia&error occurred while parsing referenceQXml<erro disparado pelo consumidorerror triggered by consumerQXmlreferncia de entidade geral analisada externa no permitida na DTD;external parsed general entity reference not allowed in DTDQXmlreferncia de entidade geral analisada externa no permitida no valor do atributoGexternal parsed general entity reference not allowed in attribute valueQXmlrreferncia de entidade geral interna no permitida na DTD4internal general entity reference not allowed in DTDQXmlVnome invlido de instruo de processamento'invalid name for processing instructionQXml(uma letra esperadaletter is expectedQXmlTmais de uma definio de tipo de documento&more than one document type definitionQXml.no ocorreu nenhum errono error occurredQXml(entidades recursivasrecursive entitiesQXmlbdeclarao nica esperada ao ler a declarao XMLAstandalone declaration expected while reading the XML declarationQXml2m combinao de etiqueta tag mismatchQXml&carcter inesperadounexpected characterQXml4fim de ficheiro inesperadounexpected end of fileQXmlnreferncia de entidade no analisada em contexto errado*unparsed entity reference in wrong contextQXmlNverso esperada ao ler a declarao XML2version expected while reading the XML declarationQXmlDvalor errado para declarao nica&wrong value for standalone declarationQXmlTreeLine/treeline.desktop0000644000175000017500000000035113262465526014457 0ustar dougdoug[Desktop Entry] Type=Application Version=1.1 Name=TreeLine GenericName=Outliner Comment=Organize text information in a tree structure Exec=treeline %f Icon=treeline-icon StartupNotify=true Terminal=false Categories=Office;TextTools; TreeLine/install.py0000755000175000017500000003735113262465526013312 0ustar dougdoug#!/usr/bin/env python3 """ **************************************************************************** install.py, Linux install script for TreeLine Copyright (C) 2018, 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/{0}'.format(progName) templateDir = 'share/{0}/templates'.format(progName) iconToolDir = 'share/icons/{0}'.format(progName) testSpell = True def usage(exitCode=2): """Display usage info and exit. Arguments: exitCode -- the code to retuen when exiting. """ global prefixDir global buildRoot print('Usage:') print(' python install.py [-h] [-p dir] [-d dir] [-t dir] [-i dir] ' '[-b dir] [-s] [-x]') print('where:') print(' -h display this help message') print(' -p dir install prefix [default: {0}]'.format(prefixDir)) print(' -d dir documentaion dir [default: /{0}]' .format(docDir)) print(' -t dir template dir [default: /{0}]' .format(templateDir)) print(' -i dir tool icon dir [default: /{0}]' .format(iconToolDir)) print(' -b dir temporary build root for packagers [default: {0}]' .format(buildRoot)) print(' -s skip language translation files') print(' -x skip all dependency checks (risky)') sys.exit(exitCode) def cmpVersions(versionStr, reqdTuple): """Return True if point-sep values in versionStr are >= reqdTuple. Arguments: versionStr -- a string with point-separated version numbers reqdTuple -- a tuple of version integers for the minimum acceptable """ match = re.search(r'[0-9\.]+', versionStr) if not match: return False 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 versionList >= reqdList: return True return False def copyDir(srcDir, dstDir): """Copy all regular files from srcDir to dstDir. dstDir is created if necessary. Arguments: srcDir -- the source dir path dstDir -- the destination dir path """ 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) as 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. Arguments: execDir -- the path where the executable is placed execName -- the name for the executable file """ text = '#!/bin/sh\n\nexec {0} {1}/{2}.py "$@"'.format(sys.executable, execDir, execName) with open(execName, 'w') as f: f.write(text) os.chmod(execName, 0o755) def replaceLine(path, origLineStart, newLine): """Replaces lines with origLineStart with newLine and rewrites the file. Arguments: path -- the file to modify origLineStart -- the beginning of the line to be replaced newLine -- the replacement line """ with open(path, 'r') as f: lines = f.readlines() with open(path, 'w') as f: for line in lines: if line.startswith(origLineStart): f.write(newLine) else: f.write(line) def spellCheck(cmdList): """Try spell checkers from list, print result. Arguments: cmdList -- a list of spell checkers to check """ 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(b'!\n') p.stdin.flush() p.stdin.close() p.stdout.close() print(' Spell Checker {0} -> OK'.format(cmd.split()[0])) return except: pass print(' Spell Checker not found -> install aspell, ispell or hunspell') 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) removeDir(os.path.join('source', '__pycache__')) global progName if os.path.isfile(progName): os.remove(progName) def removeDir(dir): """Remove dir and all files under it, ignore errors. Arguments: dir -- the directory to remove """ try: shutil.rmtree(dir, 1) except: # shouldn't be needed with ignore error param, but pass # some python versions have a bug def main(): """Main installer function. """ optLetters = 'hp:d:t:i:b:sx' try: opts, args = getopt.getopt(sys.argv[1:], optLetters) except getopt.GetoptError: usage(2) global prefixDir global docDir global templateDir global iconToolDir global buildRoot global progName depCheck = True translated = True 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': iconToolDir = val elif opt == '-b': buildRoot = val elif opt == '-s': translated = False elif opt == '-x': depCheck = False if not os.path.isfile('install.py'): print('Error - {0} files not found'.format(progName)) print('The directory containing "install.py" must be current') sys.exit(4) if (os.path.isdir('source') and not os.path.isfile('source/{0}.py'.format(progName))): print('Error - source files not found') print('Retry the extraction from the tar archive') sys.exit(4) if depCheck: print('Checking dependencies...') pyVersion = sys.version_info[:3] pyVersion = '.'.join([str(num) for num in pyVersion]) if cmpVersions(pyVersion, (3, 5)): print(' Python Version {0} -> OK'.format(pyVersion)) else: print(' Python Version {0} -> Sorry, 3.5 or higher is required' .format(pyVersion)) sys.exit(3) try: from PyQt5 import QtCore, QtWidgets except: print(' PyQt not found -> Sorry, PyQt 5.8 or higher is required' ' and must be built for Python 3') sys.exit(3) qtVersion = QtCore.qVersion() if cmpVersions(qtVersion, (5, 8)): print(' Qt Version {0} -> OK'.format(qtVersion)) else: print(' Qt Version {0} -> Sorry, 5.8 or higher is required' .format(qtVersion)) sys.exit(3) pyqtVersion = QtCore.PYQT_VERSION_STR if cmpVersions(pyqtVersion, (5, 8)): print(' PyQt Version {0} -> OK'.format(pyqtVersion)) else: print(' PyQt Version {0} -> Sorry, 5.8 or higher is required' .format(pyqtVersion)) sys.exit(3) global testSpell if testSpell: spellCheck(['aspell -a', 'ispell -a', 'hunspell -a']) pythonPrefixDir = os.path.join(prefixDir, 'share', progName) pythonBuildDir = os.path.join(buildRoot, pythonPrefixDir[1:]) if os.path.isdir('source'): print('Installing files...') print(' Copying python files to {0}'.format(pythonBuildDir)) removeDir(pythonBuildDir) # remove old? copyDir('source', pythonBuildDir) if os.path.isfile('source/plugininterface.py'): pluginBuildDir = os.path.join(pythonBuildDir, 'plugins') print(' Creating plugins directory if necessary') if not os.path.isdir(pluginBuildDir): os.makedirs(pluginBuildDir) if os.path.isdir('translations') and translated: translationDir = os.path.join(pythonBuildDir, 'translations') print(' Copying translation files to {0}'.format(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 {0}'.format(docBuildDir)) copyDir('doc', docBuildDir) if not translated: for name in glob.glob(os.path.join(docBuildDir, '*_[a-z][a-z].')): os.remove(name) # update help file location in main python script replaceLine(os.path.join(pythonBuildDir, '{0}.py'.format(progName)), 'docPath = None', 'docPath = \'{0}\' # modified by install script\n' .format(docPrefixDir)) if os.path.isdir('samples'): sampleBuildDir = os.path.join(docBuildDir, 'samples') print(' Copying sample files to {0}'.format(sampleBuildDir)) copyDir('samples', sampleBuildDir) # update sample file location in main python script replaceLine(os.path.join(pythonBuildDir, '{0}.py'.format(progName)), 'samplePath = None', 'samplePath = \'{0}\' # modified by install script\n' .format(os.path.join(docPrefixDir, 'samples'))) 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 {0}'.format(templateBuildDir)) copyDir('templates', templateBuildDir) if not translated: for name in glob.glob(os.path.join(templateBuildDir, '*.trl')): if 'en_' not in os.path.basename(name): os.remove(name) # update template file location in main python script replaceLine(os.path.join(pythonBuildDir, '{0}.py'.format(progName)), 'templatePath = None', 'templatePath = \'{0}\' # modified by install script\n' .format(templatePrefixDir)) if os.path.isdir('templates/exports'): exportsBuildDir = os.path.join(templateBuildDir, 'exports') copyDir('templates/exports', exportsBuildDir) if os.path.isdir('data'): dataPrefixDir = os.path.join(prefixDir, 'share', progName, 'data') dataBuildDir = os.path.join(buildRoot, dataPrefixDir[1:]) print(' Copying data files to {0}'.format(dataBuildDir)) removeDir(dataBuildDir) # remove old? copyDir('data', dataBuildDir) if not translated: for name in glob.glob(os.path.join(dataBuildDir, '*_[a-z][a-z].dat')): os.remove(name) # update data file location in main python script replaceLine(os.path.join(pythonBuildDir, '{0}.py'.format(progName)), 'dataFilePath = None', 'dataFilePath = \'{0}\' # modified by install script\n' .format(dataPrefixDir)) if os.path.isdir('icons'): iconPrefixDir = iconToolDir.replace('/', '') if not os.path.isabs(iconPrefixDir): iconPrefixDir = os.path.join(prefixDir, iconPrefixDir) iconBuildDir = os.path.join(buildRoot, iconPrefixDir[1:]) print(' Copying tool icon files to {0}'.format(iconBuildDir)) copyDir('icons', iconBuildDir) # update icon location in main python script replaceLine(os.path.join(pythonBuildDir, '{0}.py'.format(progName)), 'iconPath = None', 'iconPath = \'{0}\' # modified by install script\n' .format(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.isfile(os.path.join('icons', progName + '-icon.png')): pngIconPrefixDir = os.path.join(prefixDir, 'share', 'icons', 'hicolor', '48x48', 'apps') pngIconBuildDir = os.path.join(buildRoot, pngIconPrefixDir[1:]) print(' Copying app icon files to {0}'.format(pngIconBuildDir)) if not os.path.isdir(pngIconBuildDir): os.makedirs(pngIconBuildDir) shutil.copy2(os.path.join('icons', progName + '-icon.png'), pngIconBuildDir) if os.path.isfile(os.path.join('icons', progName + '-icon.svg')): svgIconPrefixDir = os.path.join(prefixDir, 'share', 'icons', 'hicolor', 'scalable', 'apps') svgIconBuildDir = os.path.join(buildRoot, svgIconPrefixDir[1:]) print(' Copying app icon files to {0}'.format(svgIconBuildDir)) if not os.path.isdir(svgIconBuildDir): os.makedirs(svgIconBuildDir) shutil.copy2(os.path.join('icons', progName + '-icon.svg'), svgIconBuildDir) if os.path.isfile(progName + '.desktop'): desktopPrefixDir = os.path.join(prefixDir, 'share', 'applications') desktopBuildDir = os.path.join(buildRoot, desktopPrefixDir[1:]) print(' Copying desktop file to {0}'.format(desktopBuildDir)) if not os.path.isdir(desktopBuildDir): os.makedirs(desktopBuildDir) shutil.copy2(progName + '.desktop', desktopBuildDir) if os.path.isdir('source'): createWrapper(pythonPrefixDir, progName) binBuildDir = os.path.join(buildRoot, prefixDir[1:], 'bin') print(' Copying executable file "{0}" to {1}' .format(progName, binBuildDir)) if not os.path.isdir(binBuildDir): os.makedirs(binBuildDir) shutil.copy2(progName, binBuildDir) compileall.compile_dir(pythonBuildDir, ddir=prefixDir) cleanSource() print('Install complete.') if __name__ == '__main__': main() TreeLine/samples/0000755000175000017500000000000013262465526012722 5ustar dougdougTreeLine/samples/120en_sample_basic_contacts.trln0000644000175000017500000001432613262465526021056 0ustar dougdoug{ "formats": [ { "fields": [ { "fieldname": "FirstName", "fieldtype": "Text" }, { "fieldname": "LastName", "fieldtype": "Text" }, { "fieldname": "Street", "fieldtype": "Text" }, { "fieldname": "City", "fieldtype": "Text" }, { "fieldname": "State", "fieldtype": "Text" }, { "fieldname": "Zip", "fieldtype": "Text" }, { "fieldname": "HomePhone", "fieldtype": "Text" }, { "fieldname": "WorkPhone", "fieldtype": "Text" }, { "fieldname": "MobilePhone", "fieldtype": "Text" }, { "fieldname": "Birthday", "fieldtype": "Date", "format": "%B %-d, %Y" }, { "fieldname": "Email", "fieldtype": "ExternalLink" } ], "formatname": "PERSON", "outputlines": [ "{*FirstName*} {*LastName*}", "{*Street*}", "{*City*}, {*State*} {*Zip*}", "{*HomePhone*} (H)", "{*WorkPhone*} (W)", "{*MobilePhone*} (M)", "DoB: {*Birthday*}", "{*Email*}" ], "titleline": "{*FirstName*} {*LastName*}" }, { "childtype": "TYPE", "fields": [ { "fieldname": "NAME", "fieldtype": "Text" } ], "formatname": "ROOT", "outputlines": [ "{*NAME*}" ], "titleline": "{*NAME*}" }, { "childtype": "PERSON", "fields": [ { "fieldname": "Type", "fieldtype": "Text" } ], "formathtml": true, "formatname": "TYPE", "outputlines": [ "
{*Type*}" ], "titleline": "{*Type*}" } ], "nodes": [ { "children": [ "fc702b0e95a311e79cb17054d2175f18", "fc703cc095a311e79cb17054d2175f18", "fc70413e95a311e79cb17054d2175f18" ], "data": { "NAME": "Main" }, "format": "ROOT", "uid": "fc7025d295a311e79cb17054d2175f18" }, { "children": [ "fc70345a95a311e79cb17054d2175f18", "fc703af495a311e79cb17054d2175f18" ], "data": { "Type": "Friends" }, "format": "TYPE", "uid": "fc702b0e95a311e79cb17054d2175f18" }, { "children": [], "data": { "Birthday": "2004-11-30", "City": "Nina", "Email": "bill@pinta.com", "FirstName": "Bill", "HomePhone": "(703) 555-5647", "LastName": "Smith", "State": "SC", "Street": "1492 Columbus Drive", "Zip": "35762" }, "format": "PERSON", "uid": "fc70345a95a311e79cb17054d2175f18" }, { "children": [], "data": { "Birthday": "1905-05-08", "City": "Harbor City", "Email": "jj@battleship.org", "FirstName": "John", "HomePhone": "(401) 555-8923", "LastName": "Johnson", "MobilePhone": "(703) 555-2873", "State": "HI", "Street": "1941 Pearl Street", "WorkPhone": "(506) 555-7413", "Zip": "86741" }, "format": "PERSON", "uid": "fc703af495a311e79cb17054d2175f18" }, { "children": [ "fc703e0a95a311e79cb17054d2175f18", "fc703fae95a311e79cb17054d2175f18" ], "data": { "Type": "Family" }, "format": "TYPE", "uid": "fc703cc095a311e79cb17054d2175f18" }, { "children": [], "data": { "Birthday": "1901-07-30", "City": "Dogbone", "Email": "jdoe@spots.net", "FirstName": "Jane", "LastName": "Doe", "MobilePhone": "(654) 555-8527", "State": "SD", "Street": "101 Dalmation Way", "Zip": "52782" }, "format": "PERSON", "uid": "fc703e0a95a311e79cb17054d2175f18" }, { "children": [], "data": { "Birthday": "1999-08-14", "City": "Undersea", "Email": "johndoe@subs.com", "FirstName": "John", "HomePhone": "(805) 555-7296", "LastName": "Doe", "MobilePhone": "(503) 555-8234", "State": "NY", "Street": "20000 Leagues Street", "WorkPhone": "(777) 555-9999", "Zip": "12763" }, "format": "PERSON", "uid": "fc703fae95a311e79cb17054d2175f18" }, { "children": [ "fc70427495a311e79cb17054d2175f18", "fc7043b495a311e79cb17054d2175f18" ], "data": { "Type": "Work" }, "format": "TYPE", "uid": "fc70413e95a311e79cb17054d2175f18" }, { "children": [], "data": { "FirstName": "Dil", "LastName": "Bert", "WorkPhone": "(501) 555-5612" }, "format": "PERSON", "uid": "fc70427495a311e79cb17054d2175f18" }, { "children": [], "data": { "FirstName": "Pointy-haired", "LastName": "Boss", "WorkPhone": "(666) 555-8945" }, "format": "PERSON", "uid": "fc7043b495a311e79cb17054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "fc7025d295a311e79cb17054d2175f18" ] } }TreeLine/samples/210en_sample_char_format.trln0000644000175000017500000000574013262465526020364 0ustar dougdoug{ "formats": [ { "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formatname": "ROOT", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "TOPIC", "fieldtype": "Text" }, { "fieldname": "TEXT", "fieldtype": "Text", "lines": 7 } ], "formathtml": true, "formatname": "TEXT_PARA", "icon": "doc", "outputlines": [ "{*TOPIC*}", "{*TEXT*}" ], "titleline": "{*TOPIC*}" } ], "nodes": [ { "children": [ "14e55e4895a411e79cb17054d2175f18", "14e5604695a411e79cb17054d2175f18", "14e5616895a411e79cb17054d2175f18", "14e5626c95a411e79cb17054d2175f18", "14e5635c95a411e79cb17054d2175f18" ], "data": { "Name": "Character Format Examples" }, "format": "ROOT", "uid": "14e559fc95a411e79cb17054d2175f18" }, { "children": [], "data": { "TEXT": "All titles can be set to bold by adding <b>...</b> tags in the Configure Data Types dialog's Output tab. The \"Allow HTML rich text in format\" option must also be checked in the Type Config tab.", "TOPIC": "Bold Titles" }, "format": "TEXT_PARA", "uid": "14e55e4895a411e79cb17054d2175f18" }, { "children": [], "data": { "TEXT": "Font formatting commands can be found in the Edit menu and in the right-click context menu in the \"Data Editor\" view.", "TOPIC": "Bold Text" }, "format": "TEXT_PARA", "uid": "14e5604695a411e79cb17054d2175f18" }, { "children": [], "data": { "TEXT": "Text can also be set to various colors.", "TOPIC": "Colored Text" }, "format": "TEXT_PARA", "uid": "14e5616895a411e79cb17054d2175f18" }, { "children": [], "data": { "TEXT": "Large text and small text", "TOPIC": "Font Size" }, "format": "TEXT_PARA", "uid": "14e5626c95a411e79cb17054d2175f18" }, { "children": [], "data": { "TEXT": "Text in italics and even text underlined", "TOPIC": "Italics and Underline" }, "format": "TEXT_PARA", "uid": "14e5635c95a411e79cb17054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "14e559fc95a411e79cb17054d2175f18" ] } }TreeLine/samples/230en_sample_intern_links.trln0000644000175000017500000000470013262465526020573 0ustar dougdoug{ "formats": [ { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Text", "fieldtype": "Text" } ], "formatname": "DEFAULT", "outputlines": [ "{*Name*}", "{*Text*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Link", "fieldtype": "InternalLink" } ], "formatname": "LINK_TYPE", "outputlines": [ "{*Name*}", "{*Link*}" ], "titleline": "{*Name*}" } ], "nodes": [ { "children": [ "2d70acb095a411e79cb17054d2175f18", "2d70aec295a411e79cb17054d2175f18", "2d70afd095a411e79cb17054d2175f18" ], "data": { "Name": "Main" }, "format": "DEFAULT", "uid": "2d70a88c95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "Target Node", "Name": "Using the external link field" }, "format": "LINK_TYPE", "uid": "2d70acb095a411e79cb17054d2175f18" }, { "children": [], "data": { "Name": "Embedded links", "Text": "Links can be embedded in any text field text by using the Internal Link item from the Edit Menu. The \"Enable click on target\" button lets you simply click on the target node." }, "format": "DEFAULT", "uid": "2d70aec295a411e79cb17054d2175f18" }, { "children": [], "data": { "Name": "Target Node", "Text": "By default, the target node gets its unique ID from the content of its first field. This can be changed in the Type Config tab of the Configure Types dialog. Use the Show Advanced button to show the control for the Unique ID Reference." }, "format": "DEFAULT", "uid": "2d70afd095a411e79cb17054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "2d70a88c95a411e79cb17054d2175f18" ] } }TreeLine/samples/140en_sample_genealogy.trln0000644000175000017500000001620613262465526020052 0ustar dougdoug{ "formats": [ { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Birth", "fieldtype": "Date", "format": "%B %-d, %Y" }, { "fieldname": "Death", "fieldtype": "Date", "format": "%B %-d, %Y" }, { "fieldname": "Location", "fieldtype": "Text" } ], "formatname": "MEMBER", "outputlines": [ "{*Name*}", "Born: {*Birth*}", "Died: {*Death*}", "Where: {*Location*}" ], "titleline": "{*Name*}" } ], "nodes": [ { "children": [ "5086f03aaf3a11e7a6243417ebd53aeb", "959dd4e8af3a11e7a4263417ebd53aeb", "a19ee912af3a11e79a1a3417ebd53aeb", "a3ed3f0aaf3a11e799293417ebd53aeb" ], "data": { "Birth": "1935-05-04", "Location": "St. Louis, MO", "Name": "Grandma Jones" }, "format": "MEMBER", "uid": "0ec2773aaf3a11e79eea3417ebd53aeb" }, { "children": [ "4b6295b4af3a11e7beaa3417ebd53aeb", "7c6a86deaf3a11e795c23417ebd53aeb", "8630c54caf3a11e7b24c3417ebd53aeb" ], "data": { "Birth": "1935-07-30", "Location": "Las Vegas, NV", "Name": "Grandpa Smith" }, "format": "MEMBER", "uid": "1b0fdaefaf3911e785d43417ebd53aeb" }, { "children": [], "data": { "Birth": "1992-07-15", "Location": "Denver, CO", "Name": "Linda" }, "format": "MEMBER", "uid": "3096a564af3b11e7b9a63417ebd53aeb" }, { "children": [], "data": { "Birth": "1995-02-14", "Location": "Boston, MA", "Name": "Billy" }, "format": "MEMBER", "uid": "35eb10b6af3b11e7b0593417ebd53aeb" }, { "children": [ "f571c05eaf3a11e793b33417ebd53aeb", "3096a564af3b11e7b9a63417ebd53aeb", "35eb10b6af3b11e7b0593417ebd53aeb" ], "data": { "Birth": "1960-08-16", "Location": "Dallas, TX", "Name": "Papa" }, "format": "MEMBER", "uid": "4b6295b4af3a11e7beaa3417ebd53aeb" }, { "children": [ "f571c05eaf3a11e793b33417ebd53aeb", "3096a564af3b11e7b9a63417ebd53aeb", "35eb10b6af3b11e7b0593417ebd53aeb" ], "data": { "Birth": "1962-02-10", "Location": "Dallas, TX", "Name": "Mama" }, "format": "MEMBER", "uid": "5086f03aaf3a11e7a6243417ebd53aeb" }, { "children": [], "data": { "Birth": "1985-12-25", "Location": "Phoenix, AZ", "Name": "Cousin Jimmy" }, "format": "MEMBER", "uid": "7baaa7baaf3b11e7bf9a3417ebd53aeb" }, { "children": [], "data": { "Birth": "2062-06-23", "Location": "Pittsburgh, PA", "Name": "Uncle Mike" }, "format": "MEMBER", "uid": "7c6a86deaf3a11e795c23417ebd53aeb" }, { "children": [], "data": { "Birth": "1987-08-31", "Location": "Houston, TX", "Name": "Cousin Dave" }, "format": "MEMBER", "uid": "81d4264aaf3b11e7b17f3417ebd53aeb" }, { "children": [ "7baaa7baaf3b11e7bf9a3417ebd53aeb", "81d4264aaf3b11e7b17f3417ebd53aeb" ], "data": { "Birth": "2065-12-08", "Location": "Cleveland, OH", "Name": "Aunt Mary" }, "format": "MEMBER", "uid": "8630c54caf3a11e7b24c3417ebd53aeb" }, { "children": [], "data": { "Birth": "1984-10-05", "Location": "New York, NY", "Name": "Cousin Joe" }, "format": "MEMBER", "uid": "916e641eaf3b11e78c293417ebd53aeb" }, { "children": [], "data": { "Birth": "2064-06-05", "Location": "Los Angelos, CA", "Name": "Aunt Patty" }, "format": "MEMBER", "uid": "959dd4e8af3a11e7a4263417ebd53aeb" }, { "children": [], "data": { "Birth": "1990-07-20", "Location": "Bar Harbor, ME", "Name": "Cousin Susan" }, "format": "MEMBER", "uid": "9bba64f8af3b11e78b123417ebd53aeb" }, { "children": [ "916e641eaf3b11e78c293417ebd53aeb", "9bba64f8af3b11e78b123417ebd53aeb" ], "data": { "Birth": "2067-11-10", "Location": "Philadelphia, PA", "Name": "Aunt Jenny" }, "format": "MEMBER", "uid": "a19ee912af3a11e79a1a3417ebd53aeb" }, { "children": [ "a4371f0caf3b11e7ac8b3417ebd53aeb" ], "data": { "Birth": "1969-05-31", "Location": "Baltimore, MD", "Name": "Uncle Bobby" }, "format": "MEMBER", "uid": "a3ed3f0aaf3a11e799293417ebd53aeb" }, { "children": [], "data": { "Birth": "1980-06-25", "Location": "Wilmington, NC", "Name": "Cousin Steve" }, "format": "MEMBER", "uid": "a4371f0caf3b11e7ac8b3417ebd53aeb" }, { "children": [ "4b6295b4af3a11e7beaa3417ebd53aeb", "7c6a86deaf3a11e795c23417ebd53aeb", "8630c54caf3a11e7b24c3417ebd53aeb" ], "data": { "Birth": "1937-04-12", "Location": "Las Vegas, NV", "Name": "Grandma Smith" }, "format": "MEMBER", "uid": "d3b3f1c8af3911e79f6f3417ebd53aeb" }, { "children": [ "5086f03aaf3a11e7a6243417ebd53aeb", "959dd4e8af3a11e7a4263417ebd53aeb", "a19ee912af3a11e79a1a3417ebd53aeb", "a3ed3f0aaf3a11e799293417ebd53aeb" ], "data": { "Birth": "1932-09-17", "Location": "St. Louis, MO", "Name": "Grandpa Jones" }, "format": "MEMBER", "uid": "f0f4f1f4af3911e790383417ebd53aeb" }, { "children": [], "data": { "Birth": "1989-05-31", "Location": "San Francisco, CA", "Name": "Me" }, "format": "MEMBER", "uid": "f571c05eaf3a11e793b33417ebd53aeb" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "1b0fdaefaf3911e785d43417ebd53aeb", "d3b3f1c8af3911e79f6f3417ebd53aeb", "f0f4f1f4af3911e790383417ebd53aeb", "0ec2773aaf3a11e79eea3417ebd53aeb" ] } }TreeLine/samples/130en_sample_basic_booklist.trln0000644000175000017500000001234513262465526021066 0ustar dougdoug{ "formats": [ { "childtype": "BOOK", "fields": [ { "fieldname": "AuthorFirstName", "fieldtype": "Text" }, { "fieldname": "AuthorLastName", "fieldtype": "Text" }, { "fieldname": "WebSite", "fieldtype": "ExternalLink", "prefix": "<", "suffix": ">" } ], "formatname": "AUTHOR", "icon": "book_1", "outputlines": [ "{*AuthorFirstName*} {*AuthorLastName*} {*WebSite*}" ], "titleline": "{*AuthorFirstName*} {*AuthorLastName*}" }, { "fields": [ { "fieldname": "Title", "fieldtype": "Text" }, { "fieldname": "Copyright", "fieldtype": "Number", "format": "0000" }, { "fieldname": "Own", "fieldtype": "Boolean", "format": "yes/no" }, { "fieldname": "ReadDate", "fieldtype": "Date", "format": "%-%-M/%-d/%Y" }, { "fieldname": "Rating", "fieldtype": "Choice", "format": "1/2/3/4/5" }, { "fieldname": "Plot", "fieldtype": "Text", "lines": 7 } ], "formatname": "BOOK", "icon": "book_3", "outputlines": [ "\"{*Title*}\"", "(c) {*Copyright*}, Own: {*Own*}", "Last Read: {*ReadDate*}, Rating: {*Rating*}", "{*Plot*}" ], "titleline": "\"{*Title*}\"" }, { "childtype": "AUTHOR", "fields": [ { "fieldname": "NAME", "fieldtype": "Text" } ], "formatname": "ROOT", "outputlines": [ "{*NAME*}" ], "titleline": "{*NAME*}" } ], "nodes": [ { "children": [ "0bf4f2ee95a411e79cb17054d2175f18", "0bf4ffbe95a411e79cb17054d2175f18" ], "data": { "NAME": "SF Books" }, "format": "ROOT", "uid": "0bf4eeb695a411e79cb17054d2175f18" }, { "children": [ "0bf4f8ca95a411e79cb17054d2175f18", "0bf4fe8895a411e79cb17054d2175f18" ], "data": { "AuthorFirstName": "Greg", "AuthorLastName": "Bear", "WebSite": "www.gregbear.com" }, "format": "AUTHOR", "uid": "0bf4f2ee95a411e79cb17054d2175f18" }, { "children": [], "data": { "Own": "false", "Plot": "Evolution caused by virus begining again", "Rating": "4", "ReadDate": "2000-10-01", "Title": "Darwin's Radio" }, "format": "BOOK", "uid": "0bf4f8ca95a411e79cb17054d2175f18" }, { "children": [], "data": { "Copyright": "1985", "Own": "true", "Plot": "Smart viruses take over", "Rating": "2", "ReadDate": "1998-07-01", "Title": "Blood Music" }, "format": "BOOK", "uid": "0bf4fe8895a411e79cb17054d2175f18" }, { "children": [ "0bf500c295a411e79cb17054d2175f18", "0bf501da95a411e79cb17054d2175f18", "0bf502de95a411e79cb17054d2175f18" ], "data": { "AuthorFirstName": "Orson Scott", "AuthorLastName": "Card", "WebSite": "www.hatrack.com" }, "format": "AUTHOR", "uid": "0bf4ffbe95a411e79cb17054d2175f18" }, { "children": [], "data": { "Copyright": "1996", "Own": "Yes", "Plot": "Time travel to change history; discovery of America", "Rating": "4", "ReadDate": "1998-09-01", "Title": "Pastwatch, The Redemption of Christopher Columbus" }, "format": "BOOK", "uid": "0bf500c295a411e79cb17054d2175f18" }, { "children": [], "data": { "Copyright": "1999", "Own": "Yes", "Plot": "Boy travels back to Russian fairy tale", "Rating": "5", "ReadDate": "2000-08-01", "Title": "Enchantment" }, "format": "BOOK", "uid": "0bf501da95a411e79cb17054d2175f18" }, { "children": [], "data": { "Copyright": "1999", "Own": "Yes", "Plot": "Ender's Game from Bean's perspective", "Rating": "5", "ReadDate": "2001-05-01", "Title": "Ender's Shadow" }, "format": "BOOK", "uid": "0bf502de95a411e79cb17054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "0bf4eeb695a411e79cb17054d2175f18" ] } }TreeLine/samples/220en_sample_bookmarks.trln0000644000175000017500000003044713262465526020072 0ustar dougdoug{ "formats": [ { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Link", "fieldtype": "ExternalLink" } ], "formatname": "BOOKMARK", "icon": "bookmark", "outputlines": [ "{*Name*}", "{*Link*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formatname": "FOLDER", "icon": "folder_3", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formathtml": true, "formatname": "SEPARATOR", "outputlines": [ "
" ], "titleline": "------------------" } ], "nodes": [ { "children": [ "25608cd495a411e79cb17054d2175f18", "25609bde95a411e79cb17054d2175f18", "2560aca095a411e79cb17054d2175f18", "2560b66495a411e79cb17054d2175f18" ], "data": { "Name": "Bookmarks Menu" }, "format": "FOLDER", "uid": "25608acc95a411e79cb17054d2175f18" }, { "children": [ "25608df695a411e79cb17054d2175f18", "2560970695a411e79cb17054d2175f18" ], "data": { "Name": "Information" }, "format": "FOLDER", "uid": "25608cd495a411e79cb17054d2175f18" }, { "children": [ "256091c095a411e79cb17054d2175f18", "256093e695a411e79cb17054d2175f18", "256094f495a411e79cb17054d2175f18", "2560960295a411e79cb17054d2175f18" ], "data": { "Name": "News" }, "format": "FOLDER", "uid": "25608df695a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.abcnews.com/", "Name": "ABC News" }, "format": "BOOKMARK", "uid": "256091c095a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.cnn.com/", "Name": "CNN.com" }, "format": "BOOKMARK", "uid": "256093e695a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.usatoday.com/", "Name": "USA Today" }, "format": "BOOKMARK", "uid": "256094f495a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://washingtonpost.com/", "Name": "WashingtonPost" }, "format": "BOOKMARK", "uid": "2560960295a411e79cb17054d2175f18" }, { "children": [ "256097f695a411e79cb17054d2175f18", "256098f095a411e79cb17054d2175f18", "256099e095a411e79cb17054d2175f18", "25609ad095a411e79cb17054d2175f18" ], "data": { "Name": "Reference" }, "format": "FOLDER", "uid": "2560970695a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://dictionary.com/", "Name": "Dictionary.com" }, "format": "BOOKMARK", "uid": "256097f695a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.dict.org/", "Name": "DICT" }, "format": "BOOKMARK", "uid": "256098f095a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://ibiblio.org/", "Name": "ibiblio" }, "format": "BOOKMARK", "uid": "256099e095a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://en.wikipedia.org/wiki/Main_Page", "Name": "Wikipedia" }, "format": "BOOKMARK", "uid": "25609ad095a411e79cb17054d2175f18" }, { "children": [ "25609cba95a411e79cb17054d2175f18", "2560a8cc95a411e79cb17054d2175f18" ], "data": { "Name": "Linux" }, "format": "FOLDER", "uid": "25609bde95a411e79cb17054d2175f18" }, { "children": [ "2560a4c695a411e79cb17054d2175f18", "2560a5de95a411e79cb17054d2175f18", "2560a6ce95a411e79cb17054d2175f18", "2560a7dc95a411e79cb17054d2175f18" ], "data": { "Name": "Debian" }, "format": "FOLDER", "uid": "25609cba95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.debian.org/", "Name": "debian.org" }, "format": "BOOKMARK", "uid": "2560a4c695a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://packages.debian.org/unstable/", "Name": "Packages in \"unstable\"" }, "format": "BOOKMARK", "uid": "2560a5de95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://packages.debian.org/experimental/", "Name": "Packages in \"experimental\"" }, "format": "BOOKMARK", "uid": "2560a6ce95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://planet.debian.net/", "Name": "Planet Debian" }, "format": "BOOKMARK", "uid": "2560a7dc95a411e79cb17054d2175f18" }, { "children": [ "2560a9bc95a411e79cb17054d2175f18", "2560aaac95a411e79cb17054d2175f18", "2560ab9295a411e79cb17054d2175f18" ], "data": { "Name": "News" }, "format": "FOLDER", "uid": "2560a8cc95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://linuxtoday.com/", "Name": "Linux Today" }, "format": "BOOKMARK", "uid": "2560a9bc95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.lwn.net/", "Name": "Linux Weekly News" }, "format": "BOOKMARK", "uid": "2560aaac95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.h-online.com/", "Name": "The H" }, "format": "BOOKMARK", "uid": "2560ab9295a411e79cb17054d2175f18" }, { "children": [ "2560ad7c95a411e79cb17054d2175f18", "2560b11495a411e79cb17054d2175f18", "2560b3bc95a411e79cb17054d2175f18" ], "data": { "Name": "Programming" }, "format": "FOLDER", "uid": "2560aca095a411e79cb17054d2175f18" }, { "children": [ "2560ae5895a411e79cb17054d2175f18", "2560af3e95a411e79cb17054d2175f18", "2560b02495a411e79cb17054d2175f18" ], "data": { "Name": "PyQt" }, "format": "FOLDER", "uid": "2560ad7c95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.diotavelli.net/PyQtWiki", "Name": "PyQt Wiki" }, "format": "BOOKMARK", "uid": "2560ae5895a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.riverbankcomputing.co.uk/", "Name": "Riverbank PyQt" }, "format": "BOOKMARK", "uid": "2560af3e95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://qt.nokia.com/", "Name": "QT Trolltech" }, "format": "BOOKMARK", "uid": "2560b02495a411e79cb17054d2175f18" }, { "children": [ "2560b1e695a411e79cb17054d2175f18", "2560b2cc95a411e79cb17054d2175f18" ], "data": { "Name": "Python" }, "format": "FOLDER", "uid": "2560b11495a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.oreillynet.com/python/", "Name": "OReilly Python" }, "format": "BOOKMARK", "uid": "2560b1e695a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.python.org/", "Name": "Python" }, "format": "BOOKMARK", "uid": "2560b2cc95a411e79cb17054d2175f18" }, { "children": [ "2560b48e95a411e79cb17054d2175f18", "2560b57495a411e79cb17054d2175f18" ], "data": { "Name": "Tools" }, "format": "FOLDER", "uid": "2560b3bc95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.selenic.com/mercurial/wiki/", "Name": "Mercurial" }, "format": "BOOKMARK", "uid": "2560b48e95a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://hgbook.red-bean.com/hgbook.html", "Name": "Mercurial Book" }, "format": "BOOKMARK", "uid": "2560b57495a411e79cb17054d2175f18" }, { "children": [ "2560b73695a411e79cb17054d2175f18", "2560b82695a411e79cb17054d2175f18", "2560b92a95a411e79cb17054d2175f18" ], "data": { "Name": "Weather" }, "format": "FOLDER", "uid": "2560b66495a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.aviationweather.gov/adds", "Name": "ADDS - Aviation Digital Data Service" }, "format": "BOOKMARK", "uid": "2560b73695a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://forecast.weather.gov/MapClick.php?lat=39.105&lon=-77.26", "Name": "NOAA" }, "format": "BOOKMARK", "uid": "2560b82695a411e79cb17054d2175f18" }, { "children": [], "data": { "Link": "http://www.wunderground.com/cgi-bin/findweather/getForecast?query=gai", "Name": "Weather Underground" }, "format": "BOOKMARK", "uid": "2560b92a95a411e79cb17054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "25608acc95a411e79cb17054d2175f18" ] } }TreeLine/samples/110en_sample_basic_longtext.trln0000644000175000017500000000732213262465526021101 0ustar dougdoug{ "formats": [ { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Text", "fieldtype": "HtmlText", "lines": 12 } ], "formathtml": true, "formatname": "HTML_TEXT", "outputlines": [ "{*Name*}", "{*Text*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Text", "fieldtype": "Text", "lines": 12 } ], "formathtml": true, "formatname": "REGULAR_TEXT", "outputlines": [ "{*Name*}", "{*Text*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formatname": "ROOT", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Text", "fieldtype": "SpacedText", "lines": 12 } ], "formathtml": true, "formatname": "SPACED_TEXT", "outputlines": [ "{*Name*}{*Text*}" ], "titleline": "{*Name*}" } ], "nodes": [ { "children": [ "ed02751e95a311e79cb17054d2175f18", "ed02771c95a311e79cb17054d2175f18", "ed027aaa95a311e79cb17054d2175f18", "ed027f0095a311e79cb17054d2175f18" ], "data": { "Name": "Text Fields" }, "format": "ROOT", "uid": "ed0270d295a311e79cb17054d2175f18" }, { "children": [], "data": { "Name": "Similar to Treepad", "Text": "This file provides a single long text field for each node. This is similar to how the Treepad program on windows is usually used." }, "format": "REGULAR_TEXT", "uid": "ed02751e95a311e79cb17054d2175f18" }, { "children": [], "data": { "Name": "Regular text field", "Text": "The most commonly used field type is regular text. Formatting such as bold, italics, font sizes and font colors can be used from the Edit menu.
\n
\nIt preserves carriage return spaces, but not multiple spaces within a line." }, "format": "REGULAR_TEXT", "uid": "ed02771c95a311e79cb17054d2175f18" }, { "children": [], "data": { "Name": "HTML text field", "Text": "An HTML field allows tags such as italics to be added manually.\n\nIt does not preserve white space.\n\nCharacters like <, >, and & must be escaped." }, "format": "HTML_TEXT", "uid": "ed027aaa95a311e79cb17054d2175f18" }, { "children": [], "data": { "Name": "Spaced text field", "Text": "A spaced text field preserves all white space.\n\nIt does not allow character formatting." }, "format": "SPACED_TEXT", "uid": "ed027f0095a311e79cb17054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "ed0270d295a311e79cb17054d2175f18" ] } }TreeLine/samples/310en_sample_conditional_todo.trln0000644000175000017500000001375113262465526021431 0ustar dougdoug{ "formats": [ { "childtype": "TASK_UNDONE", "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formathtml": true, "formatname": "ROOT", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "condition": "Done == \"true\"", "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Done", "fieldtype": "Boolean", "format": "yes/no", "init": "false" }, { "fieldname": "Urgent", "fieldtype": "Boolean", "format": "yes/no", "init": "false" } ], "formathtml": true, "formatname": "TASK_DONE", "icon": "smiley_4", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Done", "fieldtype": "Boolean", "format": "yes/no", "init": "false" }, { "fieldname": "Urgent", "fieldtype": "Boolean", "format": "yes/no", "init": "false" } ], "formathtml": true, "formatname": "TASK_UNDONE", "generic": "TASK_DONE", "icon": "smiley_2", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "condition": "Done == \"false\" and Urgent == \"true\"", "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Done", "fieldtype": "Boolean", "format": "yes/no", "init": "false" }, { "fieldname": "Urgent", "fieldtype": "Boolean", "format": "yes/no", "init": "true" } ], "formathtml": true, "formatname": "TASK_UNDONE_URGENT", "generic": "TASK_DONE", "icon": "smiley_5", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}!!!" } ], "nodes": [ { "children": [ "4295fc4e95a411e79cb17054d2175f18", "42960f7c95a411e79cb17054d2175f18" ], "data": { "Name": "Conditional Task List" }, "format": "ROOT", "uid": "4295faaa95a411e79cb17054d2175f18" }, { "children": [ "4295ff6e95a411e79cb17054d2175f18", "429602ac95a411e79cb17054d2175f18", "429603c495a411e79cb17054d2175f18", "429607b695a411e79cb17054d2175f18", "42960cca95a411e79cb17054d2175f18" ], "data": { "Name": "Home Tasks" }, "format": "ROOT", "uid": "4295fc4e95a411e79cb17054d2175f18" }, { "children": [], "data": { "Done": "false", "Name": "Mow lawn", "Urgent": "false" }, "format": "TASK_UNDONE", "uid": "4295ff6e95a411e79cb17054d2175f18" }, { "children": [], "data": { "Done": "false", "Name": "Patch wall", "Urgent": "false" }, "format": "TASK_UNDONE", "uid": "429602ac95a411e79cb17054d2175f18" }, { "children": [], "data": { "Done": "false", "Name": "Vacuum", "Urgent": "false" }, "format": "TASK_UNDONE", "uid": "429603c495a411e79cb17054d2175f18" }, { "children": [], "data": { "Done": "false", "Name": "Walk dog", "Urgent": "true" }, "format": "TASK_UNDONE_URGENT", "uid": "429607b695a411e79cb17054d2175f18" }, { "children": [], "data": { "Done": "true", "Name": "Watch TV", "Urgent": "false" }, "format": "TASK_DONE", "uid": "42960cca95a411e79cb17054d2175f18" }, { "children": [ "4296107695a411e79cb17054d2175f18", "429611b695a411e79cb17054d2175f18", "429612a695a411e79cb17054d2175f18", "4296139695a411e79cb17054d2175f18" ], "data": { "Name": "Work Tasks" }, "format": "ROOT", "uid": "42960f7c95a411e79cb17054d2175f18" }, { "children": [], "data": { "Done": "true", "Name": "Play solitaire", "Urgent": "false" }, "format": "TASK_DONE", "uid": "4296107695a411e79cb17054d2175f18" }, { "children": [], "data": { "Done": "false", "Name": "Write documents", "Urgent": "false" }, "format": "TASK_UNDONE", "uid": "429611b695a411e79cb17054d2175f18" }, { "children": [], "data": { "Done": "true", "Name": "Eat lunch", "Urgent": "false" }, "format": "TASK_DONE", "uid": "429612a695a411e79cb17054d2175f18" }, { "children": [], "data": { "Done": "false", "Name": "Compliment boss", "Urgent": "true" }, "format": "TASK_UNDONE_URGENT", "uid": "4296139695a411e79cb17054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "4295faaa95a411e79cb17054d2175f18" ] } }TreeLine/samples/240en_sample_table_booklist.trln0000644000175000017500000000660113262465526021074 0ustar dougdoug{ "formats": [ { "childtype": "BOOK", "fields": [ { "fieldname": "AuthorFirstName", "fieldtype": "Text" }, { "fieldname": "AuthorLastName", "fieldtype": "Text" }, { "fieldname": "WebSite", "fieldtype": "ExternalLink", "prefix": "<", "suffix": ">" } ], "formatname": "AUTHOR", "icon": "book_1", "outputlines": [ "{*AuthorFirstName*} {*AuthorLastName*} {*WebSite*}" ], "titleline": "{*AuthorFirstName*} {*AuthorLastName*}" }, { "fields": [ { "fieldname": "Title", "fieldtype": "Text" }, { "fieldname": "Copyright", "fieldtype": "Number", "format": "0000" }, { "fieldname": "Own", "fieldtype": "Boolean", "format": "yes/no" }, { "fieldname": "ReadDate", "fieldtype": "Date", "format": "%-m/%-d/%Y" }, { "fieldname": "Rating", "fieldtype": "Choice", "format": "1/2/3/4/5" }, { "fieldname": "Plot", "fieldtype": "Text", "lines": 7 } ], "formathtml": true, "formatname": "BOOK", "icon": "book_3", "outputlines": [ "Title: \"{*Title*}\"", "Status: (c) {*Copyright*}", "Own: {*Own*}", "Last Read: {*ReadDate*}", "Rating: {*Rating*}", "Plot: {*Plot*}" ], "spacebetween": false, "tables": true, "titleline": "\"{*Title*}\"" }, { "childtype": "AUTHOR", "fields": [ { "fieldname": "NAME", "fieldtype": "Text" } ], "formatname": "ROOT", "outputlines": [ "{*NAME*}" ], "titleline": "{*NAME*}" } ], "nodes": [ { "children": [ "5cd2946cfe1011e7b8dc7054d2175f18", "5cd2bffafe1011e7b8dc7054d2175f18" ], "data": { "NAME": "SF Books" }, "format": "ROOT", "uid": "5cd28d1efe1011e7b8dc7054d2175f18" }, { "children": [ "5cd2b3d4fe1011e7b8dc7054d2175f18", "5cd2bdc0fe1011e7b8dc7054d2175f18" ], "data": { "AuthorFirstName": "Greg", "AuthorLastName": "Bear", "WebSite": "www.gregbear.com" }, "format": "AUTHOR", "uid": "5cd2946cfe1011e7b8dc7054d2175f18" }, { "children": [], "data": { "Copyright": "1999", "Own": "false", "Plot": "Evolution caused by virus begining again", "Rating": "4", "ReadDate": "2000-10-01", "Title": "Darwin's Radio" }, "format": "BOOK", "uid": "5cd2b3d4fe1011e7b8dc7054d2175f18" }, { "children": [], "data": { "Copyright": "1985", "Own": "true", "Plot": "Smart viruses take over", "Rating": "2", "ReadDate": "1998-07-01", "Title": "Blood Music" }, "format": "BOOK", "uid": "5cd2bdc0fe1011e7b8dc7054d2175f18" }, { "children": [ "5cd2c1bcfe1011e7b8dc7054d2175f18", "5cd2c392fe1011e7b8dc7054d2175f18", "5cd2c54afe1011e7b8dc7054d2175f18" ], "data": { "AuthorFirstName": "Orson Scott", "AuthorLastName": "Card", "WebSite": "www.hatrack.com" }, "format": "AUTHOR", "uid": "5cd2bffafe1011e7b8dc7054d2175f18" }, { "children": [], "data": { "Copyright": "1996", "Own": "Yes", "Plot": "Time travel to change history; discovery of America", "Rating": "4", "ReadDate": "1998-09-01", "Title": "Pastwatch, The Redemption of Christopher Columbus" }, "format": "BOOK", "uid": "5cd2c1bcfe1011e7b8dc7054d2175f18" }, { "children": [], "data": { "Copyright": "1999", "Own": "Yes", "Plot": "Boy travels back to Russian fairy tale", "Rating": "5", "ReadDate": "2000-08-01", "Title": "Enchantment" }, "format": "BOOK", "uid": "5cd2c392fe1011e7b8dc7054d2175f18" }, { "children": [], "data": { "Copyright": "1999", "Own": "Yes", "Plot": "Ender's Game from Bean's perspective", "Rating": "5", "ReadDate": "2001-05-01", "Title": "Ender's Shadow" }, "format": "BOOK", "uid": "5cd2c54afe1011e7b8dc7054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "5cd28d1efe1011e7b8dc7054d2175f18" ] } }TreeLine/samples/320en_sample_other_fields.trln0000644000175000017500000001066413262465526020551 0ustar dougdoug{ "formats": [ { "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formathtml": true, "formatname": "DEFAULT", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "childtype": "DEFAULT", "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formathtml": true, "formatname": "FILE_INFO", "outputlines": [ "{*Name*}", "File: {*!File_Path*}/{*!File_Name*}", "File Size: {*!File_Size*} bytes", "File Modified: {*!File_Mod_Date*} {*!File_Mod_Time*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "File_Name", "fieldtype": "Text" }, { "fieldname": "File_Path", "fieldtype": "Text" }, { "fieldname": "File_Size", "fieldtype": "Number", "format": "#" }, { "fieldname": "File_Mod_Date", "fieldtype": "Date", "format": "%B %-d, %Y" }, { "fieldname": "File_Mod_Time", "fieldtype": "Time", "format": "%-I:%M:%S %p" }, { "fieldname": "File_Owner", "fieldtype": "Text" }, { "fieldname": "Page_Number", "fieldtype": "Text" }, { "fieldname": "Number_of_Pages", "fieldtype": "Text" } ], "formatname": "INT_TL_FILE_DATA_FORM", "outputlines": [ "" ], "titleline": "" }, { "childtype": "DEFAULT", "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formathtml": true, "formatname": "PARENT_CHILD", "outputlines": [ "{*Name*}", "Parent's Name: {**Name*}", "Children's Names: {*&Name*}" ], "titleline": "{*Name*}" }, { "childtype": "DEFAULT", "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formathtml": true, "formatname": "ROOT", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" } ], "nodes": [ { "children": [ "50cdc8dc95a411e79cb17054d2175f18", "50cdce0495a411e79cb17054d2175f18", "50cdcf7695a411e79cb17054d2175f18" ], "data": { "Name": "Other Field References" }, "format": "ROOT", "uid": "50cdc36e95a411e79cb17054d2175f18" }, { "children": [], "data": { "Name": "File Information" }, "format": "FILE_INFO", "uid": "50cdc8dc95a411e79cb17054d2175f18" }, { "children": [], "data": { "Name": "Parent Info" }, "format": "PARENT_CHILD", "uid": "50cdce0495a411e79cb17054d2175f18" }, { "children": [ "50cdd27895a411e79cb17054d2175f18", "50cdd3e095a411e79cb17054d2175f18", "50cdd4e495a411e79cb17054d2175f18" ], "data": { "Name": "Child Info" }, "format": "PARENT_CHILD", "uid": "50cdcf7695a411e79cb17054d2175f18" }, { "children": [], "data": { "Name": "First child" }, "format": "DEFAULT", "uid": "50cdd27895a411e79cb17054d2175f18" }, { "children": [], "data": { "Name": "Next child" }, "format": "DEFAULT", "uid": "50cdd3e095a411e79cb17054d2175f18" }, { "children": [], "data": { "Name": "Third child" }, "format": "DEFAULT", "uid": "50cdd4e495a411e79cb17054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "50cdc36e95a411e79cb17054d2175f18" ] } }TreeLine/samples/330en_sample_math_fields.trln0000644000175000017500000001564013262465526020361 0ustar dougdoug{ "formats": [ { "childtype": "PART", "fields": [ { "fieldname": "PartNumber", "fieldtype": "Text" }, { "fieldname": "Name", "fieldtype": "Text" }, { "eqn": "{**Level*} + 1", "fieldname": "Level", "fieldtype": "Math", "format": "#" }, { "fieldname": "LaborCost", "fieldtype": "Number", "format": "0.00", "prefix": "$" }, { "eqn": "sum({*&Cost*}) + {*LaborCost*}", "fieldname": "Cost", "fieldtype": "Math", "format": "0.00", "prefix": "$" }, { "eqn": "{**TotalCost*}", "fieldname": "TotalCost", "fieldtype": "Math", "format": "0.00", "prefix": "$" }, { "eqn": "{*Cost*} / {*TotalCost*} * 100", "fieldname": "PercentCost", "fieldtype": "Math", "format": "0", "suffix": "%" } ], "formatname": "ASSEMBLY", "outputlines": [ "Part {*PartNumber*}", "{*Name*}", "Assembly Level: {*Level*}", "Cost: {*Cost*}", "Percent Cost: {*PercentCost*}" ], "titleline": "{*PartNumber*} {*Name*}" }, { "fields": [ { "fieldname": "PartNumber", "fieldtype": "Text" }, { "fieldname": "Name", "fieldtype": "Text" }, { "eqn": "{**Level*} + 1", "fieldname": "Level", "fieldtype": "Math", "format": "#" }, { "fieldname": "Cost", "fieldtype": "Number", "format": "0.00", "prefix": "$" }, { "eqn": "{*Cost*} / {**TotalCost*} * 100", "fieldname": "PercentCost", "fieldtype": "Math", "format": "0", "suffix": "%" }, { "eqn": "('Low') if ({*PercentCost*} < 20) else ('High')", "fieldname": "CostLevel", "fieldtype": "Math", "format": "#.##", "resulttype": "text" } ], "formatname": "PART", "outputlines": [ "Part {*PartNumber*}", "{*Name*}", "Assembly Level: {*Level*}", "Cost: {*Cost*}", "Percent Cost: {*PercentCost*}", "Cost Level: {*CostLevel*}" ], "titleline": "{*PartNumber*} {*Name*}" }, { "childtype": "ASSEMBLY", "fields": [ { "fieldname": "PartNumber", "fieldtype": "Text" }, { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Level", "fieldtype": "Number", "format": "#" }, { "fieldname": "LaborCost", "fieldtype": "Number", "format": "0.00", "prefix": "$" }, { "eqn": "sum({*&Cost*}) + {*LaborCost*}", "fieldname": "TotalCost", "fieldtype": "Math", "format": "0.00", "prefix": "$" } ], "formatname": "TOP_ASSEMBLY", "outputlines": [ "Part {*PartNumber*}", "{*Name*}", "Assembly Level: {*Level*}", "Cost: {*TotalCost*}" ], "titleline": "{*PartNumber*} {*Name*}" } ], "nodes": [ { "children": [ "5f8ff84a95a411e79cb17054d2175f18", "5f901e5695a411e79cb17054d2175f18" ], "data": { "LaborCost": "3.5", "Level": "1", "Name": "Widget Top Assembly", "PartNumber": "123456", "TotalCost": "23.89" }, "format": "TOP_ASSEMBLY", "uid": "5f8fdd3895a411e79cb17054d2175f18" }, { "children": [ "5f900eb695a411e79cb17054d2175f18", "5f901d0c95a411e79cb17054d2175f18" ], "data": { "Cost": "10.040000000000001", "LaborCost": "1.75", "Level": "2", "Name": "Lever Assembly", "PartNumber": "456789", "PercentCost": "42.02595228128925", "TotalCost": "23.89" }, "format": "ASSEMBLY", "uid": "5f8ff84a95a411e79cb17054d2175f18" }, { "children": [], "data": { "Cost": "5.4", "CostLevel": "High", "Level": "3", "Name": "Lever", "PartNumber": "987654", "PercentCost": "22.60359983256593" }, "format": "PART", "uid": "5f900eb695a411e79cb17054d2175f18" }, { "children": [], "data": { "Cost": "2.89", "CostLevel": "Low", "Level": "3", "Name": "Lever Bolt", "PartNumber": "998877", "PercentCost": "12.097111762243616" }, "format": "PART", "uid": "5f901d0c95a411e79cb17054d2175f18" }, { "children": [ "5f901f6e95a411e79cb17054d2175f18", "5f90208695a411e79cb17054d2175f18" ], "data": { "Cost": "10.35", "LaborCost": "2.25", "Level": "2", "Name": "Bracket Assembly", "PartNumber": "112233", "PercentCost": "43.32356634575136", "TotalCost": "23.89" }, "format": "ASSEMBLY", "uid": "5f901e5695a411e79cb17054d2175f18" }, { "children": [], "data": { "Cost": "6.2", "CostLevel": "High", "Level": "3", "Name": "Bracket", "PartNumber": "445566", "PercentCost": "25.952281289242364" }, "format": "PART", "uid": "5f901f6e95a411e79cb17054d2175f18" }, { "children": [], "data": { "Cost": "1.9", "CostLevel": "Low", "Level": "3", "Name": "Bracket Pin", "PartNumber": "665544", "PercentCost": "7.95311845960653" }, "format": "PART", "uid": "5f90208695a411e79cb17054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "5f8fdd3895a411e79cb17054d2175f18" ] } }TreeLine/doc/0000755000175000017500000000000013716017505012015 5ustar dougdougTreeLine/doc/INSTALL0000644000175000017500000000113713363127527013054 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' If your distribution defaults to Python 2.x, you may need to substitute "python3", "python3.2" or "python3.3" for "python" in these commands. 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 skip dependency checks, use: 'python install.py -x'. TreeLine/doc/documentation.trln0000644000175000017500000056041113760246423015600 0ustar dougdoug{ "formats": [ { "bullets": true, "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Text", "fieldtype": "Text", "lines": 2 } ], "formathtml": true, "formatname": "BULLETS", "icon": "bullet_1", "outputlines": [ "{*Text*}" ], "spacebetween": false, "titleline": "{*Name*}" }, { "childtype": "BULLETS", "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formathtml": true, "formatname": "BULLET_HEADING", "outputlines": [ "{*Name*}" ], "spacebetween": false, "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formathtml": true, "formatname": "HEADINGS", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Text", "fieldtype": "Text", "lines": 8 } ], "formathtml": true, "formatname": "HEAD_PARA", "icon": "bullet_2", "outputlines": [ "{*Name*} - {*Text*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Text", "fieldtype": "Text", "lines": 8 } ], "formatname": "PARAGRAPH", "icon": "bullet_2", "outputlines": [ "{*Text*}" ], "titleline": "{*Name*}" } ], "nodes": [ { "children": [], "data": { "Name": "Minimize to system tray option", "Text": "A minimize to system tray option was added to General Options (under Features Available). If enabled, it adds a TreeLine system tray icon to toggle application display and hides the taskbar entry when TreeLine is minimized." }, "format": "BULLETS", "uid": "014e1392305b11e9ab99a44cc8e97404" }, { "children": [], "data": { "Name": "Automatic clone creation", "Text": "Cloned nodes can be created automatically from all identical nodes by using the \"Data > Clone All Matched Nodes\" command." }, "format": "BULLETS", "uid": "04a868f4bb0611e7aea13417ebd53aeb" }, { "children": [], "data": { "Name": "Multiple top-level nodes", "Text": "Multiple top-level (root) nodes are now permitted. When no nodes are selected in the tree (by clicking on a blank area or Ctrl clicking to unselect), the right-hand view will show information about all of the top-level nodes." }, "format": "BULLETS", "uid": "04da8386ba4811e7b3de3417ebd53aeb" }, { "children": [], "data": { "Name": "Updating multiple windows", "Text": "Properly update multiple windows after drag and drop tree changes." }, "format": "BULLETS", "uid": "0906c05bdc8f11ea94fbac675dac20af" }, { "children": [], "data": { "Name": "Reduce windows install size", "Text": "Some unnecessary libraries were eliminated from the Windows installer to reduce download sizes and installed space requirements." }, "format": "BULLETS", "uid": "10c539e2305c11e9a392a44cc8e97404" }, { "children": [], "data": { "Name": "Right view update", "Text": "Properly update the right-hand views after using a new window command." }, "format": "BULLETS", "uid": "145bd91a334c11e896b1d66a6ab671cb" }, { "children": [], "data": { "Name": "Avoid unfocused title edit", "Text": "A click on a tree node to restore tree focus no longer starts editing the node title." }, "format": "BULLETS", "uid": "15da7fde786a11e881c8a44cc8e97404" }, { "children": [], "data": { "Name": "Remove redundant newlines in files", "Text": "Some redundant newline characters were removed from text storage in TreeLine files." }, "format": "BULLETS", "uid": "192373c6305511e9ab21a44cc8e97404" }, { "children": [], "data": { "Name": "Restore data editor cursor & scroll positions", "Text": "Restore the cursor and scroll positions of data editors when the editors are re-created after focus changes." }, "format": "BULLETS", "uid": "194b052fe5d811e9a0d1a44cc8e97404" }, { "children": [ "3e749de3e5d811e98227a44cc8e97404", "194b0537e5d811e989a1a44cc8e97404", "194b0534e5d811e985f6a44cc8e97404" ], "data": { "Name": "October 6, 2019 - Release 3.1.2 (stable release)" }, "format": "HEADINGS", "uid": "194b0533e5d811e9b5fea44cc8e97404" }, { "children": [ "8cdb2406e5df11e9a06da44cc8e97404", "8e8e418ae5dc11e98404a44cc8e97404", "9139627ee5db11e98a21a44cc8e97404", "194b0536e5d811e991b3a44cc8e97404", "fcc1b718e5df11e9a7f9a44cc8e97404" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "194b0534e5d811e985f6a44cc8e97404" }, { "children": [], "data": { "Name": "Treepad import", "Text": "Fix error due to character encoding when importing files from Treepad." }, "format": "BULLETS", "uid": "194b0536e5d811e991b3a44cc8e97404" }, { "children": [ "194b052fe5d811e9a0d1a44cc8e97404", "ea0c9b24e79911e98abe7054d2175f18", "4212597ee5e011e99b51a44cc8e97404" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "194b0537e5d811e989a1a44cc8e97404" }, { "children": [], "data": { "Name": "Enable evaluate HTML tags option", "Text": "The Evaluate HTML tags control was enabled for date, time, boolean and math fields. This allows HTML tags to be evaluated or ignored in more field types." }, "format": "BULLETS", "uid": "1b73aaf4305811e98bb7a44cc8e97404" }, { "children": [], "data": { "Name": "Remove unique ID", "Text": "To improve efficiency, user visible node unique IDs that depend on the node's data have been removed." }, "format": "BULLETS", "uid": "1d015318bb0511e7a2ac3417ebd53aeb" }, { "children": [], "data": { "Name": "Reuse TreeLine windows", "Text": "When a file is opened in an existing window, the window is now reused (not just replaced) to eliminate flashing." }, "format": "BULLETS", "uid": "1e3a82e4bb0411e7b7f33417ebd53aeb" }, { "children": [], "data": { "Name": "Current date/time math field references", "Text": "Added support for current date and time references to math fields. Special field names ({*Now_Date*}, {*Now_Time*} and {*Now_Date_Time*}) must be manually typed in equations." }, "format": "BULLETS", "uid": "221ca8f0305911e99f3fa44cc8e97404" }, { "children": [], "data": { "Name": "German outline nuimbering", "Text": "Add support for German outline numbering using double letters in some levels (thanks to Teresa M)." }, "format": "BULLETS", "uid": "228fa647dc8c11eab28aac675dac20af" }, { "children": [], "data": { "Name": "Empty data edit views", "Text": "Fix a bug in Data Edit views when no fields are visible due to hidden numbering or math fields." }, "format": "BULLETS", "uid": "23b9a763dc8b11ea9dafac675dac20af" }, { "children": [], "data": { "Name": "Save as without filename", "Text": "Fix a minor bug affecting default directories for save-as and export commands when there is not already a file name set." }, "format": "BULLETS", "uid": "25ab9320482011e989f27054d2175f18" }, { "children": [], "data": { "Name": "Limit installer deletions", "Text": "In the Windows installer, improve the deletion of files from older versions and avoid problems if installing into a directory shared with other applications." }, "format": "BULLETS", "uid": "2767e530e80411e8aec9a44cc8e97404" }, { "children": [], "data": { "Name": "Math field restrictions", "Text": "Update math field equation restrictions to work with Python 3.8." }, "format": "BULLETS", "uid": "27950877dc8a11ea8902ac675dac20af" }, { "children": [], "data": { "Name": "Option to keep inaccessible recent files", "Text": "A new general option controls whether inaccessible files are removed from the recent file list at startup. The files are removed by default, but this can be changed to avoid losing listings of files stored on removable drives." }, "format": "BULLETS", "uid": "283565ba334211e8a34bd66a6ab671cb" }, { "children": [], "data": { "Name": "Breadcrumb View", "Text": "The top pane shows the parent and ancestors of the selected node. It is blank if no nodes or multiple nodes are selected. Ancestors with blue text can be clicked to select those nodes." }, "format": "HEAD_PARA", "uid": "28f8844ab58711e79d173417ebd53aeb" }, { "children": [], "data": { "Name": "Conditional type copy/paste & drag/drop", "Text": "Fixed problems with copy / paste and drag / drop on nodes with conditional type settings." }, "format": "BULLETS", "uid": "299ba9e4334a11e881a6d66a6ab671cb" }, { "children": [], "data": { "Name": "Type structure visualization", "Text": "A new Show Configuration Structure command was added to the Data menu. This opens a read-only visualization of complex type structures as another TreeLine file." }, "format": "BULLETS", "uid": "2c9ba0c832ed11e9bf7f7054d2175f18" }, { "children": [], "data": { "Name": "Pretty print output", "Text": "An indent (pretty print) output option was added to General Options (under Features Available). This makes saved TreeLine JSON files easier for humans to read at the expense of somewhat larger file sizes." }, "format": "BULLETS", "uid": "2d4430dc305711e9a33aa44cc8e97404" }, { "children": [], "data": { "Name": "Open with bad references", "Text": "TreeLine files with invalid child references can now be opened. A warning message is given about the missing child nodes in the corrupt file." }, "format": "BULLETS", "uid": "2df5b030bbf711e88a1ca44cc8e97404" }, { "children": [], "data": { "Name": "MacPorts", "Text": "See MacPorts for a third-party port to macOS." }, "format": "PARAGRAPH", "uid": "2fa1be1fdc9511eaa00dac675dac20af" }, { "children": [], "data": { "Name": "Live HTML Export", "Text": "A live tree HTML export creates an interactive view with expandable nodes and a descendant output pane." }, "format": "BULLETS", "uid": "31304c06792b11e888cfa44cc8e97404" }, { "children": [], "data": { "Name": "Restoring window geometry", "Text": "Fix issues with restoring window geometry with multiple monitors and changing configurations." }, "format": "BULLETS", "uid": "35d0914edc8911eab202ac675dac20af" }, { "children": [], "data": { "Name": "Multiple paste commands", "Text": "New paste commands have been added for adding nodes before and after siblings. There are also multiple paste commands for adding clones of nodes." }, "format": "BULLETS", "uid": "39c001f0bb0311e79e793417ebd53aeb" }, { "children": [], "data": { "Name": "Major rewrite", "Text": "This is a major rewrite of TreeLine. Once it becomes more stable, it will be released as TreeLine version 3.0.0. The 2.1.x unstable series is being discontinued (no stable 2.2.0 release is planned)." }, "format": "BULLETS", "uid": "3d2bb12eba3c11e79b223417ebd53aeb" }, { "children": [], "data": { "Name": "Chinese translation", "Text": "Add a simplified Chinese GUI translation (thanks to Qu Ray for translating)." }, "format": "BULLETS", "uid": "3e749de1e5d811e9a22ba44cc8e97404" }, { "children": [ "3e749de1e5d811e9a22ba44cc8e97404", "cef1d9fee5de11e998dea44cc8e97404" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "3e749de3e5d811e98227a44cc8e97404" }, { "children": [], "data": { "Name": "Time field AM/PM", "Text": "Now forces references to AM/PM in time field formats to be output regardless of system locale settings." }, "format": "BULLETS", "uid": "40ec6d5cbbf711e8bb93a44cc8e97404" }, { "children": [], "data": { "Name": "Outline numbering", "Text": "Change lettered outline numbering sequences to match standards. The sequences change from ...Y, Z, AA, AB, AC... to ...Y, Z, AA, BB, CC..." }, "format": "BULLETS", "uid": "4212597ee5e011e99b51a44cc8e97404" }, { "children": [], "data": { "Name": "Title list select in tree command", "Text": "A Select in Tree command was added to the Title List's right-click context menu. It selects the node corresponding to the current line in the Title List editor. This makes it easier to edit or add children to an item." }, "format": "BULLETS", "uid": "42fb4166305a11e99fd7a44cc8e97404" }, { "children": [], "data": { "Name": "Title edit mouse move", "Text": "Fixed node title editing errors caused by moving the mouse to a data edit box with the title edit still active." }, "format": "BULLETS", "uid": "45491730786a11e8b8e2a44cc8e97404" }, { "children": [], "data": { "Name": "Enter key selects current node", "Text": "The enter key now selects a tree node after the first title letter(s) are typed to make it current (shown with a box around the title)." }, "format": "BULLETS", "uid": "456aa5ca793111e89a30a44cc8e97404" }, { "children": [ "66faed8adc9411ea965eac675dac20af", "d8a76c1fdc9411eaab2dac675dac20af" ], "data": { "Name": "macOS" }, "format": "BULLET_HEADING", "uid": "460699ebdc9411ea89afac675dac20af" }, { "children": [], "data": { "Name": "Dark theme option", "Text": "An optional dark color theme was added to General Options (under Appearances)." }, "format": "BULLETS", "uid": "46f3d0a4305e11e9892fa44cc8e97404" }, { "children": [], "data": { "Name": "Relative paths", "Text": "Avoid issues resolving relative paths between different Windows drive letters in external link fields." }, "format": "BULLETS", "uid": "46fc4054334b11e89299d66a6ab671cb" }, { "children": [], "data": { "Name": "Major rewrite", "Text": "This is a major rewrite of TreeLine. Once it becomes fully stable, it will be released as TreeLine version 3.0.0." }, "format": "BULLETS", "uid": "4bc47247786711e88b26a44cc8e97404" }, { "children": [ "4bc47249786711e8a3d5a44cc8e97404", "4bc4724c786711e8a8d9a44cc8e97404", "4bc4724f786711e89bdca44cc8e97404" ], "data": { "Name": "June 30, 2018 - Release 2.9.2 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "4bc47248786711e8a8bfa44cc8e97404" }, { "children": [ "4bc4724a786711e8a4dda44cc8e97404", "4bc47247786711e88b26a44cc8e97404", "4bc4724b786711e895e2a44cc8e97404" ], "data": { "Name": "Notes" }, "format": "BULLET_HEADING", "uid": "4bc47249786711e8a3d5a44cc8e97404" }, { "children": [], "data": { "Name": "Development snapshot", "Text": "This is a development snapshot of TreeLine that should be considered a release candidate for an upcoming stable release. Many bugs have been fixed. User testing and bug reports are appreciated." }, "format": "BULLETS", "uid": "4bc4724a786711e8a4dda44cc8e97404" }, { "children": [], "data": { "Name": "Translations", "Text": "A GUI translations is available in German. All other translations are out of date and have not been included. Volunteers are needed to update translations in several languages." }, "format": "BULLETS", "uid": "4bc4724b786711e895e2a44cc8e97404" }, { "children": [ "57c4de74786911e89c3fa44cc8e97404", "71b708ac786911e88d64a44cc8e97404", "a7d7f61e786911e89bbca44cc8e97404", "15da7fde786a11e881c8a44cc8e97404", "456aa5ca793111e89a30a44cc8e97404" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "4bc4724c786711e8a8d9a44cc8e97404" }, { "children": [ "e4fc680a786911e899d3a44cc8e97404", "45491730786a11e8b8e2a44cc8e97404", "f011a5d8786911e8ace3a44cc8e97404" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "4bc4724f786711e89bdca44cc8e97404" }, { "children": [], "data": { "Name": "Multi-session controls", "Text": "Fix problems detecting existing TreeLine sessions when opening files (mostly in Linux)." }, "format": "BULLETS", "uid": "5337587ddc8811eaaaedac675dac20af" }, { "children": [], "data": { "Name": "Clone removal", "Text": "The new \"Data > Detach Clones\" command converts cloned nodes in selected branches back into independent nodes." }, "format": "BULLETS", "uid": "5647bc8cbb0611e783853417ebd53aeb" }, { "children": [], "data": { "Name": "German translation", "Text": "The German GUI translation was updated (thanks to Maria Seliger)." }, "format": "BULLETS", "uid": "57c4de74786911e89c3fa44cc8e97404" }, { "children": [], "data": { "Name": "Linux menus", "Text": "Added a desktop specification file to the Linux installer for desktop environment menu support." }, "format": "BULLETS", "uid": "58dddee4010f11e88925d66a6ab671cb" }, { "children": [], "data": { "Name": "Setting types", "Text": "Each tree node can be set to a type format independently." }, "format": "BULLETS", "uid": "5c1d58ac792a11e8b78ca44cc8e97404" }, { "children": [], "data": { "Name": "Category level swap", "Text": "A new \"Swap Category Levels\" command swaps child and grandchild nodes beneath a selected node. A child node with multiple nodes under it will become cloned under each one. Any existing grandchild clones will become individual nodes with multiple children." }, "format": "BULLETS", "uid": "5c90e0b0bb0611e78e443417ebd53aeb" }, { "children": [], "data": { "Name": "Cloned nodes", "Text": "Cloned nodes (the same nodes with multiple parents/locations) can be created using special paste commands or by automatically matching identical nodes." }, "format": "BULLETS", "uid": "5f12f768b32c11e7a41a3417ebd53aeb" }, { "children": [], "data": { "Name": "Library updates", "Text": "Update the libraries used to build the Windows binaries to Python 3.8 and Qt/PyQt 5.14." }, "format": "BULLETS", "uid": "603e11fadf2011eaa12e7054d2175f18" }, { "children": [ "2fa1be1fdc9511eaa00dac675dac20af" ], "data": { "Name": "macOS" }, "format": "BULLET_HEADING", "uid": "611e03ffdc9411ea930eac675dac20af" }, { "children": [], "data": { "Name": "TreeLine Export", "Text": "Files can be exported that are compatible with older versions of TreeLine (1.x and 2.x), with a \".trl\" file extension. Some newer features, such as cloned nodes and multiple root nodes may not be completely preserved. TreeLine subtrees can also be exported to save just selected branches of the tree to a file using the current TreeLine version." }, "format": "HEAD_PARA", "uid": "65289b36b97611e783eb3417ebd53aeb" }, { "children": [], "data": { "Name": "Custom time dialog", "Text": "Added a custom time select dialog box for editing Time and TimeDate fields." }, "format": "BULLETS", "uid": "653f2f90334c11e8a7c9d66a6ab671cb" }, { "children": [], "data": { "Name": "Printing long nodes", "Text": "Multiple issues with printing long nodes were fixed." }, "format": "BULLETS", "uid": "6610203dd2c111e8a95ad66a6ab671cb" }, { "children": [ "66102040d2c111e8a210d66a6ab671cb", "66102041d2c111e8bd85d66a6ab671cb" ], "data": { "Name": "October 20, 2018 - Release 3.0.2 (stable release)" }, "format": "HEADINGS", "uid": "6610203fd2c111e8b033d66a6ab671cb" }, { "children": [ "66102049d2c111e8bb55d66a6ab671cb" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "66102040d2c111e8a210d66a6ab671cb" }, { "children": [ "66102043d2c111e88d57d66a6ab671cb", "6610203dd2c111e8a95ad66a6ab671cb", "66102048d2c111e89f4cd66a6ab671cb" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "66102041d2c111e8bd85d66a6ab671cb" }, { "children": [], "data": { "Name": "Windows printing", "Text": "Fixed nonfunctional printing commands under Windows by building the Windows binary using PyInstaller rather than cx_Freeze." }, "format": "BULLETS", "uid": "66102043d2c111e88d57d66a6ab671cb" }, { "children": [], "data": { "Name": "Blanks as zeros", "Text": "Fixed problems saving files when the blanks as zeros property was changed for math fields." }, "format": "BULLETS", "uid": "66102048d2c111e89f4cd66a6ab671cb" }, { "children": [], "data": { "Name": "Python 3.7", "Text": "Upgraded the Windows binary from Python 3.6 to Python 3.7." }, "format": "BULLETS", "uid": "66102049d2c111e8bb55d66a6ab671cb" }, { "children": [], "data": { "Name": "Not supported", "Text": "Due to a lack of Macs for testing, TreeLine on macOS is not formally supported." }, "format": "BULLETS", "uid": "66faed8adc9411ea965eac675dac20af" }, { "children": [ "67e95a5fbbf611e8b8bda44cc8e97404", "67e95a60bbf611e8a80ca44cc8e97404" ], "data": { "Name": "September 29, 2018 - Release 3.0.1 (stable release)" }, "format": "HEADINGS", "uid": "67e95a5bbbf611e891f7a44cc8e97404" }, { "children": [ "f966db0cc23f11e8b80fd66a6ab671cb", "67e95a61bbf611e8989ca44cc8e97404" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "67e95a5fbbf611e8b8bda44cc8e97404" }, { "children": [ "67e95a64bbf611e8960fa44cc8e97404", "2df5b030bbf711e88a1ca44cc8e97404", "dc700fb4c24211e8b88dd66a6ab671cb", "40ec6d5cbbf711e8bb93a44cc8e97404", "9550a81ac24111e89029d66a6ab671cb", "71e2f25cc24211e88524d66a6ab671cb", "a757f64ac24211e8a95fd66a6ab671cb", "bdc8628ec2bd11e889e57054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "67e95a60bbf611e8a80ca44cc8e97404" }, { "children": [], "data": { "Name": "About dialog text box", "Text": "Added a read-only text box to the Help > About dialog to allow copying of the version and library information." }, "format": "BULLETS", "uid": "67e95a61bbf611e8989ca44cc8e97404" }, { "children": [], "data": { "Name": "Old file import", "Text": "Fixed a problem opening old TreeLine files that had format changes in File Info fields." }, "format": "BULLETS", "uid": "67e95a64bbf611e8960fa44cc8e97404" }, { "children": [], "data": { "Name": "Data in config structure", "Text": "Added many Show Configuration Structure data fields to show detailed settings for type formats and field formats.." }, "format": "BULLETS", "uid": "6a6067d4482011e989f27054d2175f18" }, { "children": [], "data": { "Name": "Select top node at open", "Text": "The top root node is now selected at file open if there is not a recent selection state to restore. Previously these files opened with no selection, which could cause confusion." }, "format": "BULLETS", "uid": "6a98186e305c11e99fbfa44cc8e97404" }, { "children": [], "data": { "Name": "Insert date command", "Text": "Add an Insert Date command that adds a timestamp to text field edit boxes." }, "format": "BULLETS", "uid": "6c916f15dc8c11ea9dbeac675dac20af" }, { "children": [], "data": { "Name": "Child type limits", "Text": "Child node types that are available to be set can be limited. A new pull-down list of child type limits is in the \"Type Config\" tab of the configuration dialog if the advanced functions are shown." }, "format": "BULLETS", "uid": "6dc88226bb0711e7894d3417ebd53aeb" }, { "children": [ "6df9a0c6ba3b11e7925d3417ebd53aeb", "6df9a0c9ba3b11e7a9793417ebd53aeb" ], "data": { "Name": "January 28, 2018 - Release 2.9.0 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "6df9a0c5ba3b11e78e7e3417ebd53aeb" }, { "children": [ "6df9a0c7ba3b11e7b7bd3417ebd53aeb", "3d2bb12eba3c11e79b223417ebd53aeb", "6df9a0c8ba3b11e78d783417ebd53aeb" ], "data": { "Name": "Notes" }, "format": "BULLET_HEADING", "uid": "6df9a0c6ba3b11e7925d3417ebd53aeb" }, { "children": [], "data": { "Name": "Unstable snapshot", "Text": "This is an unstable development snapshot of TreeLine. It could contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 2.0.x) is recommended for critical work." }, "format": "BULLETS", "uid": "6df9a0c7ba3b11e7b7bd3417ebd53aeb" }, { "children": [], "data": { "Name": "Translations", "Text": "The GUI and documentation translations are out of date and have not been included. Volunteers are needed to update translations in several languages." }, "format": "BULLETS", "uid": "6df9a0c8ba3b11e78d783417ebd53aeb" }, { "children": [ "6df9a0caba3b11e78b283417ebd53aeb", "d1d8e56eba4011e784ab3417ebd53aeb", "04da8386ba4811e7b3de3417ebd53aeb", "e6ad9f6eba4311e796bd3417ebd53aeb", "cd28e024bb0211e79c553417ebd53aeb", "1e3a82e4bb0411e7b7f33417ebd53aeb", "98132868ba4511e7b7283417ebd53aeb", "39c001f0bb0311e79e793417ebd53aeb", "04a868f4bb0611e7aea13417ebd53aeb", "5647bc8cbb0611e783853417ebd53aeb", "5c90e0b0bb0611e78e443417ebd53aeb", "6dc88226bb0711e7894d3417ebd53aeb", "7697b826bb0411e788433417ebd53aeb", "1d015318bb0511e7a2ac3417ebd53aeb", "f5983b38bb0411e795e13417ebd53aeb", "58dddee4010f11e88925d66a6ab671cb" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "6df9a0c9ba3b11e7a9793417ebd53aeb" }, { "children": [], "data": { "Name": "New file format", "Text": "TreeLine files now uses a new JSON format in place of the old XML format. This provides more flexibility for structuring new features like cloned nodes and multiple root nodes. A new file extension (\".trln) helps to distinguish these files." }, "format": "BULLETS", "uid": "6df9a0caba3b11e78b283417ebd53aeb" }, { "children": [], "data": { "Name": "Maintain expanded nodes", "Text": "The expand/collapse status of nearby nodes is maintained while editing the node structure. This includes adding, deleting, moving and indenting nodes." }, "format": "BULLETS", "uid": "71b708ac786911e88d64a44cc8e97404" }, { "children": [], "data": { "Name": "Complex data menu commands", "Text": "Fixed various errors when applying advanced clone and category commands from the Data menu to complex node structures." }, "format": "BULLETS", "uid": "71e2f25cc24211e88524d66a6ab671cb" }, { "children": [], "data": { "Name": "Date and time formats", "Text": "The Date, Time and DateTime fields have new format strings that enable extra text to be added to the date output." }, "format": "BULLETS", "uid": "7697b826bb0411e788433417ebd53aeb" }, { "children": [], "data": { "Name": "Number field formatting", "Text": "Fixed problems with number field formatting when using a \",\" radix with exponents." }, "format": "BULLETS", "uid": "7716c96e334a11e896bfd66a6ab671cb" }, { "children": [], "data": { "Name": "Math field expressions", "Text": "Evaluate math expressions contained in fields that are referenced by math field equations. " }, "format": "BULLETS", "uid": "7a9ace43dc8a11ea9e9bac675dac20af" }, { "children": [], "data": { "Name": "Translations", "Text": "Translations of the TreeLine GUI are available in German and Spanish. The translation should load automatically if the OS locale is properly set. Alternatively, \"--lang xx\" can be appended to the command that starts TreeLine, where \"xx\" is the two-letter language code (\"en\", \"de\" or \"es\")." }, "format": "HEAD_PARA", "uid": "7e9b3b88c24511e896c7d66a6ab671cb" }, { "children": [ "7ffeb672dc8711ea86ddac675dac20af", "7ffeb670dc8711ea81aeac675dac20af", "7ffeb66edc8711eaaa13ac675dac20af" ], "data": { "Name": "August 16, 2020 - Release 3.1.3 (stable release)" }, "format": "HEADINGS", "uid": "7ffeb66ddc8711ea9a87ac675dac20af" }, { "children": [ "d9119485dc8711ea92dcac675dac20af", "8d9c1717dc8811ea9c40ac675dac20af", "cf9c139fdc8e11ea8261ac675dac20af", "88bb6300dc8e11ea9a73ac675dac20af", "b6a4a6bddc8911ea8612ac675dac20af", "23b9a763dc8b11ea9dafac675dac20af", "0906c05bdc8f11ea94fbac675dac20af", "35d0914edc8911eab202ac675dac20af", "f2a0c0a1dc8d11ea8921ac675dac20af", "5337587ddc8811eaaaedac675dac20af" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "7ffeb66edc8711eaaa13ac675dac20af" }, { "children": [ "27950877dc8a11ea8902ac675dac20af", "c1f8dca5dc9511ea8f76ac675dac20af", "603e11fadf2011eaa12e7054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "7ffeb670dc8711ea81aeac675dac20af" }, { "children": [ "dca5b84fdc8a11eab098ac675dac20af", "6c916f15dc8c11ea9dbeac675dac20af", "7a9ace43dc8a11ea9e9bac675dac20af", "228fa647dc8c11eab28aac675dac20af" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "7ffeb672dc8711ea86ddac675dac20af" }, { "children": [ "800a0972305111e9a7fea44cc8e97404", "f7bbd909305111e9be43a44cc8e97404", "800a0975305111e9a018a44cc8e97404", "800a0976305111e9bd43a44cc8e97404" ], "data": { "Name": "February 20, 2019 - Release 3.1.0 (beta release)" }, "format": "HEADINGS", "uid": "800a0971305111e991cda44cc8e97404" }, { "children": [ "800a0973305111e9b7b9a44cc8e97404", "800a0974305111e99f0fa44cc8e97404" ], "data": { "Name": "Notes" }, "format": "BULLET_HEADING", "uid": "800a0972305111e9a7fea44cc8e97404" }, { "children": [], "data": { "Name": "Beta release", "Text": "This is a beta release of TreeLine. Some new features have not been extensively tested, but major bugs are unlikely. Testing and bug reports are appreciated, but those with very critical work should consider using the stable release (TreeLine 3.0.x)." }, "format": "BULLETS", "uid": "800a0973305111e9b7b9a44cc8e97404" }, { "children": [], "data": { "Name": "Translations", "Text": "GUI translations are available in German and Spanish, but they still require minor updates to cover recent changes. Volunteers are also needed to update or create translations in additional languages." }, "format": "BULLETS", "uid": "800a0974305111e99f0fa44cc8e97404" }, { "children": [ "6a98186e305c11e99fbfa44cc8e97404", "800a0977305111e98c38a44cc8e97404", "10c539e2305c11e9a392a44cc8e97404" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "800a0975305111e9a018a44cc8e97404" }, { "children": [ "800a097a305111e9950ba44cc8e97404", "192373c6305511e9ab21a44cc8e97404" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "800a0976305111e9bd43a44cc8e97404" }, { "children": [], "data": { "Name": "Gray out data edit view math fields", "Text": "Non-editable math fields in the Data Edit view show as gray when the mouse hovers over them." }, "format": "BULLETS", "uid": "800a0977305111e98c38a44cc8e97404" }, { "children": [], "data": { "Name": "Titles from HTML text fields", "Text": "Fixed the truncation of node titles that are generated from the first line of HTML Text fields." }, "format": "BULLETS", "uid": "800a097a305111e9950ba44cc8e97404" }, { "children": [], "data": { "Name": "Numbering", "Text": "Fix incorrect numbering updates in some situations with mixed node types." }, "format": "BULLETS", "uid": "84b24010482211e989f27054d2175f18" }, { "children": [], "data": { "Name": "Circular math errors", "Text": "Fix problems opening files that contain circular reference errors in math fields." }, "format": "BULLETS", "uid": "88bb6300dc8e11ea9a73ac675dac20af" }, { "children": [], "data": { "Name": "Truncated text export", "Text": "Fix a bug that truncated plain text exports after the first line." }, "format": "BULLETS", "uid": "8cdb2406e5df11e9a06da44cc8e97404" }, { "children": [], "data": { "Name": "Find & replace error", "Text": "Avoid an application error when a Find and Replace command causes fields to contain invalid data." }, "format": "BULLETS", "uid": "8d9c1717dc8811ea9c40ac675dac20af" }, { "children": [], "data": { "Name": "Title list select in tree", "Text": "Enable the title list view's select in tree context menu to be used on new child nodes." }, "format": "BULLETS", "uid": "8e8e418ae5dc11e98404a44cc8e97404" }, { "children": [], "data": { "Name": "Copy command errors", "Text": "Fix errors shown when using copy commands after closing TreeLine windows." }, "format": "BULLETS", "uid": "90dbc4e4334b11e892d0d66a6ab671cb" }, { "children": [], "data": { "Name": "Dark mode tooltips", "Text": "Modify dark mode colors to make tool tips visible." }, "format": "BULLETS", "uid": "9139627ee5db11e98a21a44cc8e97404" }, { "children": [], "data": { "Name": "Purge old field data", "Text": "Data from fields that were removed from the configuration is now purged when a file is saved. This avoids missing matches in the Clone All Matched Nodes command." }, "format": "BULLETS", "uid": "9550a81ac24111e89029d66a6ab671cb" }, { "children": [], "data": { "Name": "Old TreeLine Import", "Text": "Files from older versions of TreeLine (1.x or 2.x) can be imported. These files are in XML format and generally have a \".trl\" file extension (current files are in JSON format with a \".trln\" file extension). Unlike other file imports, these older files will import without showing the import dialog box when using the \"File > Open\" command." }, "format": "HEAD_PARA", "uid": "95547c38b97411e7a42a3417ebd53aeb" }, { "children": [], "data": { "Name": "Major rewrite", "Text": "This is a major rewrite of TreeLine. Once it becomes more stable, it will be released as TreeLine version 3.0.0. The 2.1.x unstable series is being discontinued (no stable 2.2.0 release is planned)." }, "format": "BULLETS", "uid": "95a6e905333f11e886edd66a6ab671cb" }, { "children": [ "95a6e90b333f11e8976fd66a6ab671cb", "95a6e90e333f11e88696d66a6ab671cb", "b16866da333f11e88adbd66a6ab671cb" ], "data": { "Name": "March 31, 2018 - Release 2.9.1 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "95a6e90a333f11e89efed66a6ab671cb" }, { "children": [ "95a6e90c333f11e8b766d66a6ab671cb", "95a6e905333f11e886edd66a6ab671cb", "95a6e90d333f11e8b88ed66a6ab671cb" ], "data": { "Name": "Notes" }, "format": "BULLET_HEADING", "uid": "95a6e90b333f11e8976fd66a6ab671cb" }, { "children": [], "data": { "Name": "Unstable snapshot", "Text": "This is an unstable development snapshot of TreeLine. It has improved but could still contain bugs. Testing and bug reports are appreciated, but those with very critical work should consider using the stable release (TreeLine 2.0.x)." }, "format": "BULLETS", "uid": "95a6e90c333f11e8b766d66a6ab671cb" }, { "children": [], "data": { "Name": "Translations", "Text": "The GUI and documentation translations are out of date and have not been included. Volunteers are needed to update translations in several languages." }, "format": "BULLETS", "uid": "95a6e90d333f11e8b88ed66a6ab671cb" }, { "children": [ "b1cd1f82334111e8b89ad66a6ab671cb", "dd22e382334111e894e6d66a6ab671cb", "653f2f90334c11e8a7c9d66a6ab671cb", "283565ba334211e8a34bd66a6ab671cb" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "95a6e90e333f11e88696d66a6ab671cb" }, { "children": [], "data": { "Name": "Child count equation references", "Text": "Eliminate a problem defining math field equations that include child count references." }, "format": "BULLETS", "uid": "95d970aa481f11e989f27054d2175f18" }, { "children": [], "data": { "Name": "Native cloned nodes", "Text": "Cloned nodes now have a more native and efficient implementation. This allows the same nodes to have multiple parents. The new breadcrumb view shows a list of clones for the selected node that can be clicked to select the other instances." }, "format": "BULLETS", "uid": "98132868ba4511e7b7283417ebd53aeb" }, { "children": [], "data": { "Name": "Format Menu", "Text": "The format menu has text formatting commands that are active when using edit boxes in the Data Edit view." }, "format": "HEAD_PARA", "uid": "997af664b58811e7ac243417ebd53aeb" }, { "children": [], "data": { "Name": "Ancestor field references", "Text": "Fixed problems with advanced ancestor field output references in some tree locations." }, "format": "BULLETS", "uid": "9a8a1400334c11e89838d66a6ab671cb" }, { "children": [], "data": { "Name": "Old TreeLine import/export", "Text": "Files from older versions of TreeLine (2.x and 1.x) can be imported and exported." }, "format": "BULLETS", "uid": "9bd80f34b32b11e7be613417ebd53aeb" }, { "children": [], "data": { "Name": "Portable config", "Text": "Problems with writing config files in Windows portable installations were fixed." }, "format": "BULLETS", "uid": "9c08f39de80311e8b791a44cc8e97404" }, { "children": [ "9c08f3a0e80311e8b2e0a44cc8e97404" ], "data": { "Name": "November 17, 2018 - Release 3.0.3 (stable release)" }, "format": "HEADINGS", "uid": "9c08f39ee80311e8a510a44cc8e97404" }, { "children": [ "9c08f3a1e80311e884dca44cc8e97404", "9c08f39de80311e8b791a44cc8e97404", "9c08f3a2e80311e88128a44cc8e97404", "2767e530e80411e8aec9a44cc8e97404" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "9c08f3a0e80311e8b2e0a44cc8e97404" }, { "children": [], "data": { "Name": "Child count fields", "Text": "Fixed an error when adding child count fields to the output format." }, "format": "BULLETS", "uid": "9c08f3a1e80311e884dca44cc8e97404" }, { "children": [], "data": { "Name": "Config file error handling", "Text": "Improved error handling after failing to read or write config files." }, "format": "BULLETS", "uid": "9c08f3a2e80311e88128a44cc8e97404" }, { "children": [], "data": { "Name": "Colors", "Text": "The default GUI colors can be changed using the \"Tools > Customize Colors\" dialog. A dark theme is available, or individual colors can be selected." }, "format": "HEAD_PARA", "uid": "9ff09734dc9011ea887bac675dac20af" }, { "children": [], "data": { "Name": "Complex undo operations", "Text": "Fixed some problems with using the Undo command after complex operations." }, "format": "BULLETS", "uid": "a757f64ac24211e8a95fd66a6ab671cb" }, { "children": [], "data": { "Name": "Save state for discarded files", "Text": "The expanded/collapsed and selected tree states are now saved even if changes were discarded in the previous session." }, "format": "BULLETS", "uid": "a7d7f61e786911e89bbca44cc8e97404" }, { "children": [], "data": { "Name": "Pull-downs on repeated dialog boxes", "Text": "Fixed issues on Windows with pull-down combo boxes not working after repeatedly showing some dialogs (mostly find and filter dialogs)." }, "format": "BULLETS", "uid": "a7fa190c334911e88d18d66a6ab671cb" }, { "children": [], "data": { "Name": "Child Type Limits", "Text": "In addition to setting the default child type, the child node types that are available to be set can be limited. There is a pull-down list of child type limits in the \"Type Config\" tab of the configuration dialog if the advanced functions are shown. Only checked types will show up in the type lists when using the \"Data > Set Node Type\" command on a child node." }, "format": "HEAD_PARA", "uid": "aa6f6ca4bb0711e7bbf43417ebd53aeb" }, { "children": [ "90dbc4e4334b11e892d0d66a6ab671cb", "299ba9e4334a11e881a6d66a6ab671cb", "e46163b4334b11e88512d66a6ab671cb", "a7fa190c334911e88d18d66a6ab671cb", "f817e5b6334a11e89349d66a6ab671cb", "7716c96e334a11e896bfd66a6ab671cb", "46fc4054334b11e89299d66a6ab671cb", "9a8a1400334c11e89838d66a6ab671cb", "145bd91a334c11e896b1d66a6ab671cb", "edd50b80334c11e8b5f5d66a6ab671cb", "b7ec7052334a11e88325d66a6ab671cb" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "b16866da333f11e88adbd66a6ab671cb" }, { "children": [], "data": { "Name": "Breadcrumb view", "Text": "An upper breadcrumb view shows titles of the selected node's ancestors; the titles can be clicked to select them in the tree." }, "format": "BULLETS", "uid": "b1b5d434b32911e780fd3417ebd53aeb" }, { "children": [], "data": { "Name": "Live tree HTML export", "Text": "A new HTML export uses Javascript and CSS to create a live tree view. Nodes in the tree can be expanded and collapsed, and a separate pane shows the output for all descendants of the selected node. One form of this export creates separate files that are linked to the existing TreeLine file. These files are intended for use on a web server (browser security usually prevents local access). Only the TreeLine file needs to be replaced to update the data (re-export is not required). The second form creates a single file (with embedded data) that can be accessed from a local web browser." }, "format": "BULLETS", "uid": "b1cd1f82334111e8b89ad66a6ab671cb" }, { "children": [], "data": { "Name": "Search and replace", "Text": "Added support for finding and replacing empty data fields using the search and replace command." }, "format": "BULLETS", "uid": "b398d8ee482211e989f27054d2175f18" }, { "children": [ "b5fe6aa030e111ebbc097054d2175f18" ], "data": { "Name": "November 27, 2020 - Release 3.1.4 (stable release)" }, "format": "HEADINGS", "uid": "b5fe698830e111ebbc097054d2175f18" }, { "children": [ "b5fe74c830e111ebbc097054d2175f18", "b5fe6f0030e111ebbc097054d2175f18", "b5fe737e30e111ebbc097054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "b5fe6aa030e111ebbc097054d2175f18" }, { "children": [], "data": { "Name": "Window closing error", "Text": "Avoid an error message when a window is closed with the focus on a dialog box." }, "format": "BULLETS", "uid": "b5fe6f0030e111ebbc097054d2175f18" }, { "children": [], "data": { "Name": "Math field root reference", "Text": "Fix a problem recalculating root references in field equations." }, "format": "BULLETS", "uid": "b5fe737e30e111ebbc097054d2175f18" }, { "children": [], "data": { "Name": "Python 3.9 compatible", "Text": "Fix an incompatibility with the new Python 3.9 version under Linux or running from source." }, "format": "BULLETS", "uid": "b5fe74c830e111ebbc097054d2175f18" }, { "children": [], "data": { "Name": "Font dialog bug", "Text": "Avoid problems with empty parameters in the font dialogs." }, "format": "BULLETS", "uid": "b6a4a6bddc8911ea8612ac675dac20af" }, { "children": [], "data": { "Name": "Output table spacing", "Text": "Fixed minor spacing issues for tables shown in output views and HTML exports." }, "format": "BULLETS", "uid": "b7ec7052334a11e88325d66a6ab671cb" }, { "children": [], "data": { "Name": "Math Fields", "Text": "For more complex output formatting, the math field can be used to combine other fields using various mathematical, relational and string operators. See the Math Type section for details." }, "format": "HEAD_PARA", "uid": "bbbf735ac24711e8b560d66a6ab671cb" }, { "children": [], "data": { "Name": "Cut last node", "Text": "Fix an error caused by attempting to use the Edit > Cut command when there is only one node." }, "format": "BULLETS", "uid": "bdc8628ec2bd11e889e57054d2175f18" }, { "children": [], "data": { "Name": "MacPorts reference", "Text": "References to a macOS port on MacPorts were added to the System Requirements and Installation documentation." }, "format": "BULLETS", "uid": "c1f8dca5dc9511ea8f76ac675dac20af" }, { "children": [], "data": { "Name": "Data editor hover", "Text": "By default, the edit boxes in the data edit view now activate when the mouse hovers over them. This eliminates the need to click to activate them. This feature can be disabled in general options if desired." }, "format": "BULLETS", "uid": "cd28e024bb0211e79c553417ebd53aeb" }, { "children": [], "data": { "Name": "Unlimited data editor height option", "Text": "Add a general option to extend the height of data editors with long text content. The default setting (limit the height to the window size) is unchanged. The new option uses the view scroll bars to access the full text length." }, "format": "BULLETS", "uid": "cef1d9fee5de11e998dea44cc8e97404" }, { "children": [], "data": { "Name": "Math field recalculation", "Text": "Perform a more complete recalculation of math fields after certain operations." }, "format": "BULLETS", "uid": "cf9c139fdc8e11ea8261ac675dac20af" }, { "children": [], "data": { "Name": "TreeLine 2 import / export", "Text": "Import and export filters are provided for older TreeLine version 2.x and 1.x files. The older files can be opened using the standard \"File > Open\" command." }, "format": "BULLETS", "uid": "d1d8e56eba4011e784ab3417ebd53aeb" }, { "children": [], "data": { "Name": "MacPorts", "Text": "There is a third-party port available on MacPorts." }, "format": "BULLETS", "uid": "d8a76c1fdc9411eaab2dac675dac20af" }, { "children": [], "data": { "Name": "Enable add child", "Text": "Make the Add Child command available after filtering has ended." }, "format": "BULLETS", "uid": "d9119485dc8711ea92dcac675dac20af" }, { "children": [ "ff3f7c98481e11e989f27054d2175f18", "db25ce66481e11e989f27054d2175f18" ], "data": { "Name": "March 17, 2019 - Release 3.1.1 (stable release)" }, "format": "HEADINGS", "uid": "db25cd62481e11e989f27054d2175f18" }, { "children": [ "e3690592481f11e989f27054d2175f18", "84b24010482211e989f27054d2175f18", "db25cf60481e11e989f27054d2175f18", "95d970aa481f11e989f27054d2175f18", "25ab9320482011e989f27054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "db25ce66481e11e989f27054d2175f18" }, { "children": [], "data": { "Name": "Math equation with copied type", "Text": "Fix problems defining a math field equation on a recently copied data type." }, "format": "BULLETS", "uid": "db25cf60481e11e989f27054d2175f18" }, { "children": [], "data": { "Name": "Sort root nodes", "Text": "The Sort command now properly sorts multiple root nodes when sorting the entire tree." }, "format": "BULLETS", "uid": "dc700fb4c24211e8b88dd66a6ab671cb" }, { "children": [], "data": { "Name": "Customize colors", "Text": "Add a more flexible tool for customizing GUI colors." }, "format": "BULLETS", "uid": "dca5b84fdc8a11eab098ac675dac20af" }, { "children": [], "data": { "Name": "Multi-level CSV import / export", "Text": "New forms of CSV table text imports and exports can work with multiple levels of child nodes, preserving the tree structure. The first column of the table includes a level number that is incremented for child relationships." }, "format": "BULLETS", "uid": "dd22e382334111e894e6d66a6ab671cb" }, { "children": [], "data": { "Name": "Dark mode printing", "Text": "Fix printing problems when using the dark theme." }, "format": "BULLETS", "uid": "e3690592481f11e989f27054d2175f18" }, { "children": [], "data": { "Name": "Error after empty filter", "Text": "Avoid problems showing the tree view after closing a filter command that found no matches." }, "format": "BULLETS", "uid": "e46163b4334b11e88512d66a6ab671cb" }, { "children": [], "data": { "Name": "Deletion errors", "Text": "Several errors occurring when deleting or removing nodes have been fixed. These include problems when the mouse is pre-highlighting descendant nodes and when nodes to be deleted are selected in a separate window." }, "format": "BULLETS", "uid": "e4fc680a786911e899d3a44cc8e97404" }, { "children": [], "data": { "Name": "Breadcrumb view", "Text": "A new breadcrumb view pane above the other panes shows a sequence of ancestor titles for a selected node. The ancestors can be clicked to select them. For cloned nodes, multiple lines provide links to each instance's ancestor sequence." }, "format": "BULLETS", "uid": "e6ad9f6eba4311e796bd3417ebd53aeb" }, { "children": [], "data": { "Name": "Windows binary", "Text": "The Windows binary is now built using updated libraries (Qt 5.11 and PyQt 5.11)." }, "format": "BULLETS", "uid": "e7776f4da14511e88aefa44cc8e97404" }, { "children": [], "data": { "Name": "Major rewrite", "Text": "This is the first stable release of a major TreeLine rewrite. It includes all of the new features shown below in the 2.1.x and 2.9.x development series." }, "format": "BULLETS", "uid": "e8e10d32a14311e8ab90a44cc8e97404" }, { "children": [ "e8e10d34a14311e899cca44cc8e97404", "e8e10d38a14311e8a0c3a44cc8e97404", "e8e10d37a14311e8b0b3a44cc8e97404" ], "data": { "Name": "August 19, 2018 - Release 3.0.0 (new stable release)" }, "format": "HEADINGS", "uid": "e8e10d33a14311e88e24a44cc8e97404" }, { "children": [ "e8e10d32a14311e8ab90a44cc8e97404", "e8e10d35a14311e8aca9a44cc8e97404", "e8e10d36a14311e8b832a44cc8e97404" ], "data": { "Name": "Notes" }, "format": "BULLET_HEADING", "uid": "e8e10d34a14311e899cca44cc8e97404" }, { "children": [], "data": { "Name": "Compatibility", "Text": "TreeLine files now use a JSON format in place of the old XML format. This provides more flexibility for structuring new features like cloned nodes and multiple root nodes. A new file extension (\".trln) helps to distinguish files with the new format. Import and export filters are provided for older TreeLine version 2.x and 1.x files. The older files can be opened using the standard \"File > Open\" command." }, "format": "BULLETS", "uid": "e8e10d35a14311e8aca9a44cc8e97404" }, { "children": [], "data": { "Name": "Translations", "Text": "A GUI translations is available in German. All other translations are out of date and have not been included. Volunteers are needed to update translations in several languages." }, "format": "BULLETS", "uid": "e8e10d36a14311e8b832a44cc8e97404" }, { "children": [ "e7776f4da14511e88aefa44cc8e97404" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "e8e10d37a14311e8b0b3a44cc8e97404" }, { "children": [ "e8e10d3ca14311e8b71da44cc8e97404" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "e8e10d38a14311e8a0c3a44cc8e97404" }, { "children": [], "data": { "Name": "Default font", "Text": "The default font for the application can now be set in Tools > Customize Fonts. This is useful for enlarging fonts on some systems with high screen resolutions. Options still exist to individually specify tree view, output view and editor fonts." }, "format": "BULLETS", "uid": "e8e10d3ca14311e8b71da44cc8e97404" }, { "children": [], "data": { "Name": "Add modified flag", "Text": "Add an asterisk after the file name in the title bar if a file has been modified." }, "format": "BULLETS", "uid": "ea0c9b24e79911e98abe7054d2175f18" }, { "children": [], "data": { "Name": "Category command disable", "Text": "Avoid problems running data category-based commands on selections without any child nodes." }, "format": "BULLETS", "uid": "edd50b80334c11e8b5f5d66a6ab671cb" }, { "children": [], "data": { "Name": "Initial field values", "Text": "Fixed errors occurring when field types are changed after initial default values have been set." }, "format": "BULLETS", "uid": "f011a5d8786911e8ace3a44cc8e97404" }, { "children": [], "data": { "Name": "Cloned nodes", "Text": "Nodes that are duplicated by cloning show up as separate clickable paths in the upper breadcrumb view." }, "format": "BULLETS", "uid": "f08b97be792b11e88588a44cc8e97404" }, { "children": [ "f16f7f70a25a11e7b7c67054d2175f18", "f16f87b8a25a11e7b7c67054d2175f18", "f16fbef4a25a11e7b7c67054d2175f18", "f16fc7aaa25a11e7b7c67054d2175f18", "f16fd236a25a11e7b7c67054d2175f18", "f16fe6fea25a11e7b7c67054d2175f18", "f1703d8ea25a11e7b7c67054d2175f18", "f1713464a25a11e7b7c67054d2175f18" ], "data": { "Name": "TreeLine Documentation" }, "format": "HEADINGS", "uid": "f16f7d90a25a11e7b7c67054d2175f18" }, { "children": [ "f16f82aea25a11e7b7c67054d2175f18", "f16f8498a25a11e7b7c67054d2175f18", "f16f85b0a25a11e7b7c67054d2175f18", "f16f86aaa25a11e7b7c67054d2175f18" ], "data": { "Name": "Introduction" }, "format": "HEADINGS", "uid": "f16f7f70a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Version", "Text": "This document covers TreeLine, Version 3.1.4, a stable release, dated November 28, 2020 by Doug Bell." }, "format": "PARAGRAPH", "uid": "f16f82aea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Why TreeLine?", "Text": "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." }, "format": "PARAGRAPH", "uid": "f16f8498a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Outliner vs. database", "Text": "TreeLine is both an outliner and a small database. It 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 database. The output format for each node can be defined, and the output can be shown on the screen, printed, or exported to HTML or text." }, "format": "PARAGRAPH", "uid": "f16f85b0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "License", "Text": "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 stays non-proprietary. 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." }, "format": "PARAGRAPH", "uid": "f16f86aaa25a11e7b7c67054d2175f18" }, { "children": [ "f16f8aeca25a11e7b7c67054d2175f18", "f16f9654a25a11e7b7c67054d2175f18", "f16f9c44a25a11e7b7c67054d2175f18", "f16fa040a25a11e7b7c67054d2175f18", "f16fa31aa25a11e7b7c67054d2175f18", "f16fa806a25a11e7b7c67054d2175f18", "f16fadf6a25a11e7b7c67054d2175f18", "f16fb1dea25a11e7b7c67054d2175f18", "f16fb90ea25a11e7b7c67054d2175f18" ], "data": { "Name": "Features" }, "format": "HEADINGS", "uid": "f16f87b8a25a11e7b7c67054d2175f18" }, { "children": [ "f16f9050a25a11e7b7c67054d2175f18", "f16f921ca25a11e7b7c67054d2175f18", "f16f932aa25a11e7b7c67054d2175f18", "f16f9424a25a11e7b7c67054d2175f18", "f16f9550a25a11e7b7c67054d2175f18" ], "data": { "Name": "General" }, "format": "BULLET_HEADING", "uid": "f16f8aeca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Store information", "Text": "Stores almost any type of information, including plain text, rich text, HTML, numbers, dates, times, booleans, URLs, etc." }, "format": "BULLETS", "uid": "f16f9050a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Tree structure", "Text": "The tree structure helps keep things organized." }, "format": "BULLETS", "uid": "f16f921ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Fields", "Text": "Each node can have several fields that form a mini-database." }, "format": "BULLETS", "uid": "f16f932aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Node types", "Text": "Several node types, with different sets of fields, can be included in one file." }, "format": "BULLETS", "uid": "f16f9424a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Output format", "Text": "The node format, including fields, output lines, formatting and tree-view icon, can be defined for each node type." }, "format": "BULLETS", "uid": "f16f9550a25a11e7b7c67054d2175f18" }, { "children": [ "f16f9744a25a11e7b7c67054d2175f18", "f16f9834a25a11e7b7c67054d2175f18", "f16f992ea25a11e7b7c67054d2175f18", "f16f9a32a25a11e7b7c67054d2175f18", "f16f9b36a25a11e7b7c67054d2175f18", "b1b5d434b32911e780fd3417ebd53aeb" ], "data": { "Name": "Views" }, "format": "BULLET_HEADING", "uid": "f16f9654a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Tree view", "Text": "The left-hand view shows an indented, expandable list of titles" }, "format": "BULLETS", "uid": "f16f9744a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Right-hand views", "Text": "The right-hand view can show one of three views - for showing output, editing node data and editing node titles." }, "format": "BULLETS", "uid": "f16f9834a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Show parent and children", "Text": "The right-hand view is normally split to show data from the parent node and its children." }, "format": "BULLETS", "uid": "f16f992ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Show multiple selection", "Text": "If multiple nodes are selected, the right-hand view shows all of their data." }, "format": "BULLETS", "uid": "f16f9a32a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Show descendant output", "Text": "The output view can be set to show indented output from all descendant nodes." }, "format": "BULLETS", "uid": "f16f9b36a25a11e7b7c67054d2175f18" }, { "children": [ "f16f9d2aa25a11e7b7c67054d2175f18", "f16f9e2ea25a11e7b7c67054d2175f18", "f16f9f3ca25a11e7b7c67054d2175f18" ], "data": { "Name": "Navigation" }, "format": "BULLET_HEADING", "uid": "f16f9c44a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Find commands", "Text": "Find commands can search node data for text matches or for more specific rules." }, "format": "BULLETS", "uid": "f16f9d2aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Filtering", "Text": "Filtering commands show only matching nodes in a flat left-hand view." }, "format": "BULLETS", "uid": "f16f9e2ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Toggle selections", "Text": "Previous and next selection commands toggle selections to quickly move between parts of the tree." }, "format": "BULLETS", "uid": "f16f9f3ca25a11e7b7c67054d2175f18" }, { "children": [ "f16fa130a25a11e7b7c67054d2175f18", "5c1d58ac792a11e8b78ca44cc8e97404", "f16fa220a25a11e7b7c67054d2175f18" ], "data": { "Name": "Formatting" }, "format": "BULLET_HEADING", "uid": "f16fa040a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Configuration dialog", "Text": "The dialog for data type configuration has several tabs to easily set all type, field and output parameters." }, "format": "BULLETS", "uid": "f16fa130a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Format copies", "Text": "Formatting information can be copied from another TreeLine file." }, "format": "BULLETS", "uid": "f16fa220a25a11e7b7c67054d2175f18" }, { "children": [ "f16fa40aa25a11e7b7c67054d2175f18", "f16fa504a25a11e7b7c67054d2175f18", "f16fa5fea25a11e7b7c67054d2175f18", "f16fa70ca25a11e7b7c67054d2175f18" ], "data": { "Name": "File Handling" }, "format": "BULLET_HEADING", "uid": "f16fa31aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Undo/redo", "Text": "Undo and redo commands are available for all modifying operations." }, "format": "BULLETS", "uid": "f16fa40aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File formats", "Text": "TreeLine files use a JSON format, with options for automatically compressing or encrypting the files." }, "format": "BULLETS", "uid": "f16fa504a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Document templates", "Text": "Document templates for new files are preformatted to cover basic needs." }, "format": "BULLETS", "uid": "f16fa5fea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Printing", "Text": "The formatted output can be printed with parent/child lines, headers and footers." }, "format": "BULLETS", "uid": "f16fa70ca25a11e7b7c67054d2175f18" }, { "children": [ "f16fa8f6a25a11e7b7c67054d2175f18", "31304c06792b11e888cfa44cc8e97404", "f16fa9f0a25a11e7b7c67054d2175f18", "9bd80f34b32b11e7be613417ebd53aeb", "f16faaf4a25a11e7b7c67054d2175f18", "f16fabf8a25a11e7b7c67054d2175f18", "f16facf2a25a11e7b7c67054d2175f18" ], "data": { "Name": "File Import and Export" }, "format": "BULLET_HEADING", "uid": "f16fa806a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "HTML export", "Text": "The data can be exported to single or multiple HTML files with optional navigation panes." }, "format": "BULLETS", "uid": "f16fa8f6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Text import/export", "Text": "Plain text, tab-indented text and delimited table files can be imported and exported." }, "format": "BULLETS", "uid": "f16fa9f0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Bookmark import/export", "Text": "Mozilla and XBEL format bookmark files can be imported and exported." }, "format": "BULLETS", "uid": "f16faaf4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Generic XML import/export", "Text": "Generic XML files can be imported and exported, allowing TreeLine to function as a crude XML editor." }, "format": "BULLETS", "uid": "f16fabf8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "ODF import/export", "Text": "ODF text documents can be imported and exported as outlines." }, "format": "BULLETS", "uid": "f16facf2a25a11e7b7c67054d2175f18" }, { "children": [ "f16faee6a25a11e7b7c67054d2175f18", "f16fafe0a25a11e7b7c67054d2175f18", "f16fb0daa25a11e7b7c67054d2175f18", "f08b97be792b11e88588a44cc8e97404" ], "data": { "Name": "Linking" }, "format": "BULLET_HEADING", "uid": "f16fadf6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Internal links", "Text": "Internal link fields select a linked node when clicked." }, "format": "BULLETS", "uid": "f16faee6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "External links", "Text": "External link fields can be used to open URLs in web browsers." }, "format": "BULLETS", "uid": "f16fafe0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Embedded links", "Text": "Both internal and external links can be embedded into text fields." }, "format": "BULLETS", "uid": "f16fb0daa25a11e7b7c67054d2175f18" }, { "children": [ "f16fb2cea25a11e7b7c67054d2175f18", "f16fb3c8a25a11e7b7c67054d2175f18", "f16fb4eaa25a11e7b7c67054d2175f18", "f16fb5e4a25a11e7b7c67054d2175f18", "5f12f768b32c11e7a41a3417ebd53aeb", "f16fb6dea25a11e7b7c67054d2175f18", "f16fb7f6a25a11e7b7c67054d2175f18" ], "data": { "Name": "Data Manipulation" }, "format": "BULLET_HEADING", "uid": "f16fb1dea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Sorting", "Text": "Nodes can be sorted by title or by predefined key fields." }, "format": "BULLETS", "uid": "f16fb2cea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Math Fields", "Text": "Math fields can be defined that automatically calculate their contents based on numerical values in other nodes." }, "format": "BULLETS", "uid": "f16fb3c8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Numbering", "Text": "Numbering fields can be defined and automatically updated." }, "format": "BULLETS", "uid": "f16fb4eaa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Conditional types", "Text": "A node's icon and output format can be changed conditionally based on its data." }, "format": "BULLETS", "uid": "f16fb5e4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Spell check", "Text": "Text data can be spell checked (requires an external program - see the System Requirements section)." }, "format": "BULLETS", "uid": "f16fb6dea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Arranging data", "Text": "Data can be automatically re-arranged using categories from data fields." }, "format": "BULLETS", "uid": "f16fb7f6a25a11e7b7c67054d2175f18" }, { "children": [ "f16fba08a25a11e7b7c67054d2175f18", "f16fbb02a25a11e7b7c67054d2175f18", "f16fbc06a25a11e7b7c67054d2175f18", "f16fbd00a25a11e7b7c67054d2175f18" ], "data": { "Name": "Customization" }, "format": "BULLET_HEADING", "uid": "f16fb90ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Customization", "Text": "There are many options for customizing both general and file-based attributes." }, "format": "BULLETS", "uid": "f16fba08a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Shortcuts and toolbars", "Text": "There are editors for keyboard shortcuts and toolbar commands." }, "format": "BULLETS", "uid": "f16fbb02a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Fonts", "Text": "Fonts used in the GUI, editors and output views can be customized." }, "format": "BULLETS", "uid": "f16fbc06a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Languages", "Text": "The user interface is available in simplified Chinese, English, German and Spanish. Translations into other languages are TBD." }, "format": "BULLETS", "uid": "f16fbd00a25a11e7b7c67054d2175f18" }, { "children": [ "f16fbfeea25a11e7b7c67054d2175f18", "f16fc4bca25a11e7b7c67054d2175f18", "460699ebdc9411ea89afac675dac20af" ], "data": { "Name": "System Requirements" }, "format": "HEADINGS", "uid": "f16fbef4a25a11e7b7c67054d2175f18" }, { "children": [ "f16fc0d4a25a11e7b7c67054d2175f18", "f16fc1cea25a11e7b7c67054d2175f18", "f16fc2c8a25a11e7b7c67054d2175f18", "f16fc3c2a25a11e7b7c67054d2175f18" ], "data": { "Name": "Linux" }, "format": "BULLET_HEADING", "uid": "f16fbfeea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Python", "Text": "Python (Version 3.5 or higher)" }, "format": "BULLETS", "uid": "f16fc0d4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Qt", "Text": "QT (Version 5.8 or higher)" }, "format": "BULLETS", "uid": "f16fc1cea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "PyQt", "Text": "PyQt (Version 5.8 or higher)" }, "format": "BULLETS", "uid": "f16fc2c8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Spell check", "Text": "If spell checking is desired, either aspell, ispell or hunspell are required" }, "format": "BULLETS", "uid": "f16fc3c2a25a11e7b7c67054d2175f18" }, { "children": [ "f16fc5aca25a11e7b7c67054d2175f18", "f16fc6b0a25a11e7b7c67054d2175f18" ], "data": { "Name": "Windows" }, "format": "BULLET_HEADING", "uid": "f16fc4bca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Binary", "Text": "Should run on any computer running Windows Vista, 7, 8 or 10." }, "format": "BULLETS", "uid": "f16fc5aca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Spell check", "Text": "If spell checking is desired, an external program is required. Either aspell, ispell or hunspell must be installed." }, "format": "BULLETS", "uid": "f16fc6b0a25a11e7b7c67054d2175f18" }, { "children": [ "f16fc89aa25a11e7b7c67054d2175f18", "f16fcc96a25a11e7b7c67054d2175f18", "611e03ffdc9411ea930eac675dac20af" ], "data": { "Name": "Installation" }, "format": "HEADINGS", "uid": "f16fc7aaa25a11e7b7c67054d2175f18" }, { "children": [ "f16fc98aa25a11e7b7c67054d2175f18", "f16fcaa2a25a11e7b7c67054d2175f18", "f16fcb9ca25a11e7b7c67054d2175f18" ], "data": { "Name": "Linux" }, "format": "HEADINGS", "uid": "f16fc89aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Basic installation", "Text": "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\"." }, "format": "PARAGRAPH", "uid": "f16fc98aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Python 3", "Text": "If your distribution defaults to Python 2.x, you may need to substitute \"python3\" for \"python\" in these commands." }, "format": "PARAGRAPH", "uid": "f16fcaa2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Install options", "Text": "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 skip dependency checks, use: \"python install.py -x\"." }, "format": "PARAGRAPH", "uid": "f16fcb9ca25a11e7b7c67054d2175f18" }, { "children": [ "f16fcf3ea25a11e7b7c67054d2175f18", "f16fd042a25a11e7b7c67054d2175f18", "f16fd132a25a11e7b7c67054d2175f18" ], "data": { "Name": "Windows" }, "format": "HEADINGS", "uid": "f16fcc96a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "All users", "Text": "To install for all users, execute the \"TreeLine-x.x.x-install-all.exe\" file. Administrator permissions are required." }, "format": "PARAGRAPH", "uid": "f16fcf3ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Single user", "Text": "To install for a single user (administrator rights are not required), execute the \"TreeLine-x.x.x-install-user.exe\" file." }, "format": "PARAGRAPH", "uid": "f16fd042a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Portable install", "Text": "For a portable install, execute the \"TreeLine-x.x.x-install-user.exe\" file. The file association, shortcuts and uninstaller tasks should be unchecked. When TreeLine starts and prompts for the config file location, choose the program directory option." }, "format": "PARAGRAPH", "uid": "f16fd132a25a11e7b7c67054d2175f18" }, { "children": [ "f16fd326a25a11e7b7c67054d2175f18", "f16fdccca25a11e7b7c67054d2175f18", "f16fe0b4a25a11e7b7c67054d2175f18", "f16fe3fca25a11e7b7c67054d2175f18" ], "data": { "Name": "Basic Usage" }, "format": "HEADINGS", "uid": "f16fd236a25a11e7b7c67054d2175f18" }, { "children": [ "f16fd682a25a11e7b7c67054d2175f18", "28f8844ab58711e79d173417ebd53aeb", "f16fd858a25a11e7b7c67054d2175f18", "f16fd9aca25a11e7b7c67054d2175f18", "f16fdabaa25a11e7b7c67054d2175f18", "f16fdbc8a25a11e7b7c67054d2175f18" ], "data": { "Name": "Views" }, "format": "HEADINGS", "uid": "f16fd326a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Tree View", "Text": "The left-hand view shows a tree of node titles. Parent nodes can be opened and closed to display or hide their indented descendant nodes. Clicking on an already selected node allows the title to be edited. Right-click context menus are available for commonly used functions." }, "format": "HEAD_PARA", "uid": "f16fd682a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Right-hand Views", "Text": "The right pane is tabbed to show one of three different views of the data. The \"Data Output\" view shows the formatted text, the \"Data Edit\" view shows text edit boxes, and the \"Title List\" view shows an editable list of node titles.
\n
\nWhen 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 a 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.
\n
\nWhen multiple nodes are selected in the tree (by holding down the shift or Ctrl keys while clicking), the right view will not display any child node information. It will instead show information about every selected node.
\n
\nWhen no nodes are selected in the tree (by clicking on a blank area or Ctrl clicking to unselect), the right view will show information about the top-level (root) nodes." }, "format": "HEAD_PARA", "uid": "f16fd858a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Data Output View", "Text": "The \"Data Output\" view shows formatted output text. It cannot be edited from this view.
\n
\nWhen the \"View > Show Output Descendants\" command is toggled, the \"Data Output\" view will show an indented list with information about every descendant of a single selected node." }, "format": "HEAD_PARA", "uid": "f16fd9aca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Data Edit View", "Text": "The \"Data Edit\" view shows a text edit box for each data field within a node. It also shows the node types and the node titles. The types of edit boxes vary based on the field type. Some are just text editors, while others (such as choice fields, date fields, links, etc.) have pull-down menus or dialogs." }, "format": "HEAD_PARA", "uid": "f16fdabaa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Title List View", "Text": "The \"Title List\" view shows a list of node titles that can be modified using typical text editor methods. If a new line is typed, a new node is created with that title. If a line is deleted, the corresponding node is removed from the tree." }, "format": "HEAD_PARA", "uid": "f16fdbc8a25a11e7b7c67054d2175f18" }, { "children": [ "f16fddbca25a11e7b7c67054d2175f18", "f16fdeb6a25a11e7b7c67054d2175f18", "997af664b58811e7ac243417ebd53aeb", "f16fdfb0a25a11e7b7c67054d2175f18" ], "data": { "Name": "Editing" }, "format": "HEADINGS", "uid": "f16fdccca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Node Menu", "Text": "The commands in the \"Node\" menu operate on the selected nodes in the left tree view. There are commands to add or insert nodes, rename node titles and delete nodes. There are also commands to rearrange the tree by changing indent levels or moving nodes up or down. For many of the commands, the descendants of the selected nodes are also affected." }, "format": "HEAD_PARA", "uid": "f16fddbca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Edit Menu", "Text": "The edit menu includes undo and redo commands that can fix problems. Cut, copy and paste commands can operate either on text in the right-hand view (if selected or active) or to tree nodes." }, "format": "HEAD_PARA", "uid": "f16fdeb6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Shortcuts", "Text": "There are several shortcuts for use in tree editing. Drag and drop will move (or copy if the Ctrl button is held) nodes. Clicking on a selected node will rename it. Pressing the delete key will remove the selected nodes. If desired, these shortcuts can be disabled in \"Tools > General Options\"." }, "format": "HEAD_PARA", "uid": "f16fdfb0a25a11e7b7c67054d2175f18" }, { "children": [ "f16fe1cca25a11e7b7c67054d2175f18", "f16fe2c6a25a11e7b7c67054d2175f18" ], "data": { "Name": "Files" }, "format": "HEADINGS", "uid": "f16fe0b4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Templates", "Text": "When starting a new file, a dialog box offers a choice of templates. The default has only a single text field for each node that contains the title. The Long Text template adds a second long text field for more output text. Other templates have various fields for contacts, book lists and to-do lists." }, "format": "HEAD_PARA", "uid": "f16fe1cca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Sample Files", "Text": "Various TreeLine sample files can be opened by using the \"File > Open Sample\" command. These have more detail and example content than the new file templates." }, "format": "HEAD_PARA", "uid": "f16fe2c6a25a11e7b7c67054d2175f18" }, { "children": [ "f16fe4eca25a11e7b7c67054d2175f18", "f16fe5f0a25a11e7b7c67054d2175f18" ], "data": { "Name": "Data Types" }, "format": "HEADINGS", "uid": "f16fe3fca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Node Types", "Text": "Multiple node data types can be defined in a TreeLine file. Each can contain different data fields and have different output formats. See the template and sample files for examples. Nodes can be set to a specific type using the \"Data > Set Node Type\" command." }, "format": "HEAD_PARA", "uid": "f16fe4eca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Type Config", "Text": "The \"Data > Configure Data Types\" command is used to modify node data types, fields and output formatting. Refer to the Detailed Usage section of the full documentation for details." }, "format": "HEAD_PARA", "uid": "f16fe5f0a25a11e7b7c67054d2175f18" }, { "children": [ "f16fe7e4a25a11e7b7c67054d2175f18", "f16fed0ca25a11e7b7c67054d2175f18", "f16ff2f2a25a11e7b7c67054d2175f18", "f1700936a25a11e7b7c67054d2175f18", "f1700f58a25a11e7b7c67054d2175f18", "f170146ca25a11e7b7c67054d2175f18", "f1701caaa25a11e7b7c67054d2175f18", "f170207ea25a11e7b7c67054d2175f18", "f170256aa25a11e7b7c67054d2175f18", "f1702c4aa25a11e7b7c67054d2175f18", "f170335ca25a11e7b7c67054d2175f18" ], "data": { "Name": "Detailed Usage" }, "format": "HEADINGS", "uid": "f16fe6fea25a11e7b7c67054d2175f18" }, { "children": [ "f16fe8dea25a11e7b7c67054d2175f18", "f16fe9e2a25a11e7b7c67054d2175f18", "f16feaf0a25a11e7b7c67054d2175f18", "f16fec08a25a11e7b7c67054d2175f18" ], "data": { "Name": "Tree Navigation and Search" }, "format": "HEADINGS", "uid": "f16fe7e4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Keyboard Shortcuts", "Text": "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. The \"Home\", \"End\", \"Page Up\" and \"Page Down\" keys can be used to move quickly through the tree.
\n
\nAnother way to move through the tree is to type the first letter of a visible node title. Hitting the letter again highlights to the next possibility." }, "format": "HEAD_PARA", "uid": "f16fe8dea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Selection", "Text": "Multiple nodes can be selected by holding down the CTRL or the SHIFT key 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.
\n
\nThe \"View > Previous Selection\" and \"View > Next Selection\" commands can be used to toggle through a history of selections, allowing faster navigation through the tree." }, "format": "HEAD_PARA", "uid": "f16fe9e2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Searching", "Text": "The \"Tools > Find Text\" command will search for text within the tree structure. The dialog box has options for searching all of the node data or only the node titles. There are also options for how to interpret the search text. Key words will match nodes with the search words found anywhere in the node. Key full words will only match complete words anywhere in the node. Full phrase will only match the complete phrase in the proper sequence. Finally, the regular expression option will search using Python regular expressions.
\n
\nThe \"Tools > Conditional Find\" command will search in particular node types and node fields. Various comparison operators can be selected to exactly match, to match a greater or lesser value, or part of the value. And the True/False operators give the same result regardless of the values. In general, the value is interpreted using the edit format for special field types. Multiple rules can be added, connected with logical \"and\" or \"or\" operators. The \"All Types\" option makes fields from every type are available, so that multiple node types to be part of the same search. The condition will be false for node types that do not contain that field name.
\n
\nThere is also a quick, incremental search of node titles. By default, it's bound to ctrl+/. Then, matching titles are found as the search string is typed. The F3 and shift+F3 keys can be used to go to the next or previous matches, respectively." }, "format": "HEAD_PARA", "uid": "f16feaf0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Filtering", "Text": "There are two filtering commands, \"Tools > Text Filter\" and \"Tools > Conditional Filter\". They work like the corresponding search commands above, except that they show all of the matching nodes in a flat list that replaces the tree view. The nodes can be selected and edited from this view. Use the \"End Filter\" button to restore the full tree view.
\n
\nSearches for Conditional Find and Conditional Filter can be saved in the dialog boxes and loaded again at another time." }, "format": "HEAD_PARA", "uid": "f16fec08a25a11e7b7c67054d2175f18" }, { "children": [ "f16fedfca25a11e7b7c67054d2175f18", "f16feef6a25a11e7b7c67054d2175f18", "f16feffaa25a11e7b7c67054d2175f18", "f16ff0f4a25a11e7b7c67054d2175f18", "f16ff1e4a25a11e7b7c67054d2175f18" ], "data": { "Name": "Defining Node Types" }, "format": "HEADINGS", "uid": "f16fed0ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Type List", "Text": "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." }, "format": "HEAD_PARA", "uid": "f16fedfca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Type Config", "Text": "\"Type Config\" is the second tab of the 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.
\n
\nThe \"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.
\n
\nThere are also options here for adding blanks lines between nodes, allowing HTML tags in the common format text, and changing the output to add bullets or tables." }, "format": "HEAD_PARA", "uid": "f16feef6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Field List", "Text": "The \"Field List\" is the third tab of the 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, renamed or deleted. Sort keys can also be defined to specify the fields that are compared when nodes are sorted." }, "format": "HEAD_PARA", "uid": "f16feffaa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Field Config", "Text": "\"Field Config\" is the fourth tab of the Configure Types Dialog. The field type and its output format string can be set, if applicable to the field. 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. The number of lines displayed in the editor for the field can also be specified. Finally, an option to evaluate HTML tags can be set to recognize HTML tags in Choice, AutoChoice, Combination, AutoCombination and RegularExpression fields. If this is set, special characters (angled brackets and quotation marks) will need to be manually escaped to show up in the field text." }, "format": "HEAD_PARA", "uid": "f16ff0f4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Output", "Text": "\"Output\" is the last tab of the Configure Types Dialog. 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 fields that are selected in the list (multiple fields can be selected by holding Ctrl or Shift keys while clicking) can be added to a format at the cursor position with the \">>\" keys. The field reference at the cursor can be removed with the \"<<\" keys." }, "format": "HEAD_PARA", "uid": "f16ff1e4a25a11e7b7c67054d2175f18" }, { "children": [ "f16ff3e2a25a11e7b7c67054d2175f18", "f16ff4dca25a11e7b7c67054d2175f18", "f16ff5f4a25a11e7b7c67054d2175f18", "f16ff7c0a25a11e7b7c67054d2175f18", "f16ff8f6a25a11e7b7c67054d2175f18", "f16ff9faa25a11e7b7c67054d2175f18", "f17005f8a25a11e7b7c67054d2175f18", "f1700710a25a11e7b7c67054d2175f18", "f16ffc02a25a11e7b7c67054d2175f18", "f16ffd06a25a11e7b7c67054d2175f18", "f16ffe14a25a11e7b7c67054d2175f18", "f16ffb08a25a11e7b7c67054d2175f18", "f16fff18a25a11e7b7c67054d2175f18", "f1700008a25a11e7b7c67054d2175f18", "f1700102a25a11e7b7c67054d2175f18", "f1700206a25a11e7b7c67054d2175f18", "f1700300a25a11e7b7c67054d2175f18", "f1700404a25a11e7b7c67054d2175f18", "f17004fea25a11e7b7c67054d2175f18", "f170081ea25a11e7b7c67054d2175f18" ], "data": { "Name": "Field Types" }, "format": "HEADINGS", "uid": "f16ff2f2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Field Options", "Text": "The field type and options are set in the \"Field Config\" tab of the \"Data > Configure Types Dialog\". The many different field types are described in the paragraphs below.
\n
\nSeveral 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 a blue triangle to show in the upper left corner of the edit box, and the output for that field will be replaced with \"#####\"." }, "format": "HEAD_PARA", "uid": "f16ff3e2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Text Type", "Text": "The default field type is a text field. It is the most commonly used field. These fields are edited using edit boxes in the data editor view. There are several commands in the Format menu (and also in the context menu) for setting the font style and adding external or internal links. The edit box height expands when re-displayed after adding several lines of text. The minimum edit box height can also be set explicitly in the \"Field Config\" tab." }, "format": "HEAD_PARA", "uid": "f16ff4dca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "HTML Text Type", "Text": "This type allows simple HTML tags to be used in the text. Commonly used tags include \"<b>bold</b>\", \"<u>underline</u>\", \"line break<br/>\", \"horizontal line<hr/>\", and various font tags. Complex block tags should generally be avoided. Carriage returns are ignored, and non-escaped \"<\", \">\" and \"&\" symbols do not display. " }, "format": "HEAD_PARA", "uid": "f16ff5f4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "One Line Text Type", "Text": "This type restricts the text length to a single line. It does not allow carriage returns but does not restrict line wrapping of a single long line." }, "format": "HEAD_PARA", "uid": "f16ff7c0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Spaced Text Type", "Text": "This type holds plain text and preserves all spacing. Other formatting of the text is not permitted. It could be useful to use the \"Tools > Customize Fonts\" command to set the editor font to a mono-spaced font when using this field type." }, "format": "HEAD_PARA", "uid": "f16ff8f6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Number Type", "Text": "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.
\n
\nThe 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.
\n
\nUnlike 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)." }, "format": "HEAD_PARA", "uid": "f16ff9faa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Boolean Type", "Text": "This type gives two choices corresponding to true/false values. The format help menu includes typical values such as \"yes/no\", \"true/false\" and \"1/0\", but users can also enter their own word pair. The data editor boxes will accept either the currently set format or any of the typical values." }, "format": "HEAD_PARA", "uid": "f16ffb08a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Date Type", "Text": "In the date field type, special characters in the format (all starting with \"%\") are replaced by elements of the data, similar to number fields. Press the \"Format Help\" button from the field format dialog for formatting details. Non-special characters will be output as themselves.
\n
\nThere is also an edit format under \"Tools > General Options > Data Editor Formats\". This controls how date 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.
\n
\nA default initial field value of \"Now\" can be used to enter the date of node creation." }, "format": "HEAD_PARA", "uid": "f16ffc02a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Time Type", "Text": "In the time field type, special characters in the format (all starting with \"%\") are replaced by elements of the data, similar to number fields. Press the \"Format Help\" button from the field format dialog for formatting details. Non-special characters will be output as themselves.
\n
\nThere is also an edit format under \"Tools > General Options > Data Editor Formats\". This controls how time 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.
\n
\nA default initial field value of \"Now\" can be used to enter the time of node creation." }, "format": "HEAD_PARA", "uid": "f16ffd06a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "DateTime Type", "Text": "The DateTime field combines dates and times into a single field that is useful for timestamps. Special characters in the format (all starting with \"%\") are replaced by elements of the data, as in date and time fields. Press the \"Format Help\" button from the field format dialog for formatting details. Non-special characters will be output as themselves.
\n
\nThe DateTime edit format is uses the date and time formats located in \"Tools > General Options > Data Editor Formats\". The date and time formats are combined, separated by a space character.
\n
\nA default initial field value of \"Now\" can be used to enter the date and time of node creation." }, "format": "HEAD_PARA", "uid": "f16ffe14a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Choice Type", "Text": "The choice field type allows 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). Entries in the data editor which do not match the choices will cause a blue triangle to show in the upper left corner of the edit box, and the output for that field will be replaced with \"#####\"." }, "format": "HEAD_PARA", "uid": "f16fff18a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Auto Choice Type", "Text": "This field type is similar to the choice type, but without a format string. The entries in the pull-down menu are automatically generated from all previously used entries. Any entries that are typed will be available in the pull-down menu for future use. " }, "format": "HEAD_PARA", "uid": "f1700008a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Combination Type", "Text": "This is basically the equivalent of the choice type with multiple selection. The formatting string entries are separated by the \"/\" character. The pull-down menu shows check-boxes that are checked for currently selected nodes. By default, the selected entries are output separated by a comma and a space. This can be changed in the \"Type Config\" tab of the \"Data > Configure Types Dialog\". Click the \"Show Advanced\" button to see the separator setting." }, "format": "HEAD_PARA", "uid": "f1700102a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Auto Combination Type", "Text": "This field type is similar to the combination type, but without a format string. The entries in the pull-down menu are automatically generated from all previously used entries. Any entries that are typed will be available in the pull-down menu for future use. " }, "format": "HEAD_PARA", "uid": "f1700206a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "External Link Type", "Text": "This type can support various link protocols, including http and https for web pages, mailto for email addresses and file for local files. A pull down dialog in the data editor allows the selection of the protocol type and the entry of an address and a display name. In the edit boxes, the display name shows up in [brackets], and it is used as the text of the link in the output view. The \"file\" protocol also provides a button to browse for a path and buttons to choose either absolute or relative path names.
\n
\nClicking on the link in the output view or choosing \"Open Link\" from the right-click context menu in the edit box will open the link in a web browser. In Linux, setting the \"BROWSER\" environment variable to a string like \"mozilla %s\" will result in the desired browser being used." }, "format": "HEAD_PARA", "uid": "f1700300a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Internal Link Type", "Text": "These links create shortcuts to select nodes elsewhere in the tree structure. To create a link, click the pull-down arrow and then select the target node in the tree view. The display name is shown in brackets. It is initially set to the target node title, but it can be edited. The right-click context menu can be used to clear the link or to select the target node.
\n
\nClicking the link in the output view will select the target node in the tree view." }, "format": "HEAD_PARA", "uid": "f1700404a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Picture Type", "Text": "These links add referenced pictures to the output view. A pull down dialog in the data editor has a button to browse for picture files to be linked. It also allows absolute or relative paths to be used and has a small image preview." }, "format": "HEAD_PARA", "uid": "f17004fea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Math Type", "Text": "Math field types are configured by defining equations. The equations can reference number fields, date fields, time fields, boolean fields, text fields and/or other math fields. The resulting values of math fields are automatically calculated for each node. Various mathematical, relational and string operators are available. The results can be numbers, text, boolean, dates or times.

To define a math field equation, press the \"Define Equation\" button in the \"Field Config\" tab of the \"Data > Configure Types Dialog\". This brings up a dialog with fields to reference on the left and math operators on the right. The \"Reference Level\" pull-down determines whether the reference is from the same node, the node's parent, the root node, or the node's children. The \"Result Type\" pull-down allows arithmetic, date, time, boolean or text results to be chosen. The \"Operator Type\" pull-down allows numeric, comparison or text operators to be shown in the operator list. The down-arrow buttons below the references and operators add the selected item to the equation text below. Portions of equations can also be typed directly in the equation text line editor.

References to child nodes must be enclosed in a grouping function, such as sum, max, min, mean or join. A math field can contain a parent or child reference to itself, but not a same-level reference to itself (a circular reference). The references only contain the field name, so they will reference a parent or child field with that name even if it is a different node type.

In equations, date fields are represented by the number of days since January 1, 1970, and time fields are the number of seconds since midnight. So date fields can be subtracted to give the number of days elapsed, and numbers of days can be added to or subtracted from dates to result in new dates. Time fields can be subtracted to give the number of seconds elapsed, and numbers of seconds can be added to or subtracted from times to result in new times. Also, special field names ({*Now_Date*}, {*Now_Time*} and {*Now_Date_Time*}) can be manually typed in equations to represent current dates and times. The \"Data->Regenerate References\" command may be needed to force re-evaluation of time-based references.

The \"if\" comparison operator can be used to make the result depend on the value of another field. The expression is written as \"true_value if condition else false_value\". Of course, the \"true_value\", \"condition\" and \"false_value\" strings must be replaced with valid fields or expressions. If this operator is inserted from the operator list (under comparisons), it will include parenthesis as placeholders. These parenthesis are optional in the equations.

The \"join\" text function is used to combine text from several other fields (or from child nodes). The first argument to the function is a separator string that is placed between each piece of text. The remaining argument(s) are the text to be joined.

By default, math fields are shown in the data edit view, but they are read-only. To hide them, uncheck the \"Show math fields in the Data Edit View\" box under \"Tools > General Options > Features Available\".

There is also an option under \"File > Properties\" that toggles whether blank fields are treated as zeros. If checked (the default), a blank field that is referenced by a math field has a value of zero (for numeric operations) or blank (for text operations). If unchecked, any blank references also cause equations that reference them to be blank.
" }, "format": "HEAD_PARA", "uid": "f17005f8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Numbering Type", "Text": "This type (not to be confused with the number type above) provides fields that are automatically filled in with the \"Data > Update Numbering\" command. The \"Format Help\" button in the field format dialog shows the output format options. A single format level will result in a simple sequential numbering scheme. Use of the \"/\" level separator will result in an outline-type numbering with different sequences at different levels. Use of the \".\" section separator will result in a \"2.3.5\" type numbering scheme.
\n
\nSince numbering fields are automatically populated, by default they are not shown in the data edit view. To show them, check the \"Show numbering fields in the Data Edit View\" box under \"Tools > General Options > Features Available\". When they are shown in the data edit view, they show up in section numbering style, regardless of the output format." }, "format": "HEAD_PARA", "uid": "f1700710a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Regular Expression Type", "Text": "This type allows arbitrary format strings to be matched that restrict the data to a particular format. It uses Python regular expression syntax. Entries in the data editor which do not match the format string expression will cause a blue triangle to show in the upper left corner of the edit box, and the output for that field will be replaced with \"#####\"." }, "format": "HEAD_PARA", "uid": "f170081ea25a11e7b7c67054d2175f18" }, { "children": [ "f1700a26a25a11e7b7c67054d2175f18", "f1700b34a25a11e7b7c67054d2175f18", "f1700c38a25a11e7b7c67054d2175f18", "f1700d3ca25a11e7b7c67054d2175f18", "f1700e40a25a11e7b7c67054d2175f18", "bbbf735ac24711e8b560d66a6ab671cb" ], "data": { "Name": "Output Formatting Details" }, "format": "HEADINGS", "uid": "f1700936a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Output Format Examples", "Text": "Here is an example of output formatting for a book list:
\n
\n\"{*Title*}\"
\n(c) {*Copyright*}, Rating: {*Rating*}
\n{*PlotDescription*}
\n
\nEach of the field names in enclosed in {* *}, curly brackets and asterisks. For more examples, see the sample files that can be opened using the \"File > Open Sample\" command." }, "format": "HEAD_PARA", "uid": "f1700a26a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Title Formats", "Text": "When a node in the tree is renamed, the program attempts to match the title formatting pattern to set the appropriate fields (the same title matching occurs when editing lines in the \"Title List\" view). 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.
\n
\nIf the text data used for a tree view title has multiple lines, only the first line will be used as the title." }, "format": "HEAD_PARA", "uid": "f1700b34a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Skipped Output Lines", "Text": "If all of the fields in an output format line are empty for a given node, then 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." }, "format": "HEAD_PARA", "uid": "f1700c38a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "HTML Tags", "Text": "By default, the \"Allow HTML rich text in formats\" option is unchecked in the \"Type Config\" tab of the Configure Types Dialog. So any HTML tags are treated as plain text. If the option is enabled, simple HTML formatting tags can be used in node output formats.
\n
\nCommonly used tags include \"<b>bold</b>\", \"<u>underline</u>\", \"line break<br/>\", \"horizontal line<hr/>\", and various font tags. Complex block tags should generally be avoided." }, "format": "HEAD_PARA", "uid": "f1700d3ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Other Field References", "Text": "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.
\n
\nIf 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, modified date and modified time. These special fields are shown as \"{*!field_name*}\" in the title and output format editors.
\n
\nThere 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.
\n
\nReferences to child nodes can also be added, shown as \"{*&field_name*}\". 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 on the \"Type Config\" tab (with show advanced active). The separator defaults to a comma and a space, but can be set to <br/> or anything else.
\n
\nFinally, 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.
\n
\nFor examples of these fields, see the \"sample_other_fields\" file (by using the \"File > Open Sample\" command)." }, "format": "HEAD_PARA", "uid": "f1700e40a25a11e7b7c67054d2175f18" }, { "children": [ "f1701156a25a11e7b7c67054d2175f18", "f170125aa25a11e7b7c67054d2175f18", "f1701368a25a11e7b7c67054d2175f18", "aa6f6ca4bb0711e7bbf43417ebd53aeb" ], "data": { "Name": "Type Format Details" }, "format": "HEADINGS", "uid": "f1700f58a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Generic and Derived Types", "Text": "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.
\n
\nThere 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." }, "format": "HEAD_PARA", "uid": "f1701156a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Conditional Types", "Text": "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. 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 \"Data->Regenerate References\" command may be needed to force re-evaluation of time-based references.

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 rather than a false one.

For an example, see the \"sample_conditional_todo\" file (by using the \"File > Open Sample\" command)." }, "format": "HEAD_PARA", "uid": "f170125aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Copying Formats", "Text": "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." }, "format": "HEAD_PARA", "uid": "f1701368a25a11e7b7c67054d2175f18" }, { "children": [ "f170155ca25a11e7b7c67054d2175f18", "f1701660a25a11e7b7c67054d2175f18", "f1701764a25a11e7b7c67054d2175f18", "f1701868a25a11e7b7c67054d2175f18", "f170198aa25a11e7b7c67054d2175f18", "f1701a8ea25a11e7b7c67054d2175f18" ], "data": { "Name": "Tree Data Operations" }, "format": "HEADINGS", "uid": "f170146ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Sorting", "Text": "The \"Data > Sort Nodes\" command can sort nodes based on either node titles or key fields predefined in the node type configuration. The predefined sort fields can be changed in the \"Field Config\" tab of the Configure Types Dialog. The \"Sort Keys\" button brings up a list of fields that define a sort key sequence. The fields higher in the sequence have a higher priority. The direction for each key field can be flipped." }, "format": "HEAD_PARA", "uid": "f170155ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Numbering", "Text": "The \"Data > Update Numbering\" command updates the contents of fields with a special numbering field type. The field's output format defines how the numbers are displayed in the output, including whether individual numbers, outline numbers or a section numbering scheme are shown. See the Numbering Field Type for more information.
\n
\nNote that numbering fields are not shown in the data edit view unless \"Show numbering fields in the Data Edit View\" is checked under \"Tools > General Options > Features Available\". When they are shown in the data edit view, they show up in section numbering style, regardless of the output format." }, "format": "HEAD_PARA", "uid": "f1701660a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Find and Replace", "Text": "The \"Tools > Find and Replace\" command can be used to change the text in several nodes. The search text and replacement text are entered. Searching can be based on any match, full words only, or a Python regular expression. The operation can optionally be restricted to a particular node type and to a particular node field.
\n
\nReplacement using regular expressions is quite powerful. Searching for \".*\" will match all of the text in the field. The replacement string can contain back references that consist of a backslash and a number. The back references get replaced with the corresponding parenthesized group from the match. For example, \"\\2\" will be replaced with the text that matched the second group of parenthesis. The \"\\g<0>\" back-reference can be used to substitute the entire matching string." }, "format": "HEAD_PARA", "uid": "f1701764a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Spell Check", "Text": "There is a spell check command in the \"Tools\" menu. Use of this command requires an external program to be installed (either aspell, ispell or hunspell- 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." }, "format": "HEAD_PARA", "uid": "f1701868a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Cloned Nodes", "Text": "Cloned nodes are used to duplicate sections of the tree. Editing their data or child structure in one location changes all locations. When a cloned node is selected, multiple lines are shown in the Breadcrumb view, showing ancestor nodes that have multiple parents. The selected node will appear black (not a link), but its clones can be clicked to select their location in the tree view.
\n
\nCloned nodes are created by copying nodes or branches and then using the special paste clone commands found in the \"Edit\" menu to paste them elsewhere. The clone link can be removed by deleting the nodes or by using the \"Data > Detach Clones\" command to convert them back to regular nodes.
\n
\nCloned nodes can also be created automatically by using the \"Data > Clone All Matched Nodes\" command. This will convert all identical nodes in the tree structure into cloned nodes.
\n
\nFor an example of cloned nodes with multiple parents, see the \"sample_genealogy\" file (by using the \"File > Open Sample\" command)." }, "format": "HEAD_PARA", "uid": "f170198aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Category-Based Arrangement", "Text": "The \"Data\" menu contains commands for arranging and flattening the data by category. These methods are used to automatically add and remove levels of nodes below the current node in the tree.
\n
\nThe \"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.
\n
\nThe \"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.
\n
\nThere is also a \"Swap Category Levels\" command that will swap the child and grandchild nodes beneath a selected node. A child node with multiple nodes under it will become cloned under each one. Any existing grandchild clones will become individual nodes with multiple children." }, "format": "HEAD_PARA", "uid": "f1701a8ea25a11e7b7c67054d2175f18" }, { "children": [ "f1701d90a25a11e7b7c67054d2175f18", "f1701e8aa25a11e7b7c67054d2175f18", "f1701f84a25a11e7b7c67054d2175f18" ], "data": { "Name": "Printing" }, "format": "HEADINGS", "uid": "f1701caaa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Print Setup", "Text": "The dialog for print setup contains four tabs. The first, General Options, includes settings for what part of the tree to print, whether to include lines to child nodes and whether to allow page breaks between a parent an its first child node. It also has place to set the printer queue, to make pagination and print previews more accurate. The second tab, Page Setup, includes paper size, orientation, margins and columns. The third tab selects the font for printing. The last tab, Header/Footer, defines text and file data meta-fields for inclusion in headers and footers." }, "format": "HEAD_PARA", "uid": "f1701d90a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Print Preview", "Text": "The Print Preview window shows how the printout will look with current print settings. It can be dragged larger to show more detail, or the zoom settings can be changed. It also includes buttons for the Print Setup dialog and for printing." }, "format": "HEAD_PARA", "uid": "f1701e8aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Print to PDF", "Text": "This command prompts for a path and file name to export a PDF file. It uses the current printer settings." }, "format": "HEAD_PARA", "uid": "f1701f84a25a11e7b7c67054d2175f18" }, { "children": [ "f170216ea25a11e7b7c67054d2175f18", "f1702272a25a11e7b7c67054d2175f18", "f170236ca25a11e7b7c67054d2175f18", "f1702470a25a11e7b7c67054d2175f18" ], "data": { "Name": "File Handling" }, "format": "HEADINGS", "uid": "f170207ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File Compression", "Text": "A TreeLine file is in a JSON text format. There are options to compress the files (gzip format) to save storage space. Individual files can be set to compressed mode from either \"File > Properties\" or from the file type pull-down in the save-as dialog." }, "format": "HEAD_PARA", "uid": "f170216ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File Encryption", "Text": "There is a file encryption option to password protect TreeLine files. Individual files can be set to encrypted mode from either \"File > Properties\" or from the file type pull-down in the save-as dialog. The encryption uses an SHA hash function as a stream cipher - it should be fairly secure." }, "format": "HEAD_PARA", "uid": "f1702272a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Auto-Save", "Text": "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." }, "format": "HEAD_PARA", "uid": "f170236ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Saved Tree States", "Text": "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 files. If desired, this feature can be disabled with a general option." }, "format": "HEAD_PARA", "uid": "f1702470a25a11e7b7c67054d2175f18" }, { "children": [ "f170265aa25a11e7b7c67054d2175f18", "f170275ea25a11e7b7c67054d2175f18", "f1702b46a25a11e7b7c67054d2175f18", "95547c38b97411e7a42a3417ebd53aeb", "f1702858a25a11e7b7c67054d2175f18", "f1702952a25a11e7b7c67054d2175f18", "f1702a42a25a11e7b7c67054d2175f18" ], "data": { "Name": "File Import" }, "format": "HEADINGS", "uid": "f170256aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "General Information", "Text": "A TreeLine file is in a specific JSON text format. Other types of files can be imported using the \"File > Import\" command, which will show a dialog box where the type of import can be selected. Alternatively, using the \"File > Open\" command with a non-TreeLine file will also show this dialog." }, "format": "HEAD_PARA", "uid": "f170265aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Text Import", "Text": "Several different text formats can be selected for import. Tab indented text creates a node title from each line in the file, structured based on the number of tabs before each line.
\n
\nThe comma-delimited (CSV) and the tab delimited text tables use the first line as a header row to create field names, then each additional row becomes a node with field data taken from each column. The CSV import with level numbers creates a tree structure from level numbers in the first column that are incremented to show child relationships.
\n
\nThe plain text, one node per line import creates a flat tree of node titles.
\n
\nFinally, the plain text paragraph import creates long text nodes from text separated by blank lines." }, "format": "HEAD_PARA", "uid": "f170275ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Treepad Import", "Text": "Files from the Treepad shareware program can be imported. Only Treepad text nodes are supported." }, "format": "HEAD_PARA", "uid": "f1702858a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "XML Import", "Text": "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." }, "format": "HEAD_PARA", "uid": "f1702952a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "ODF Import", "Text": "TreeLine will import Open Document Format (ODF) text documents, from applications such as Apache OpenOffice and LibreOffice. 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." }, "format": "HEAD_PARA", "uid": "f1702a42a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Bookmarks Import", "Text": "TreeLine will import bookmark files in both the Mozilla HTML format (Firefox browser) 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).
\n" }, "format": "HEAD_PARA", "uid": "f1702b46a25a11e7b7c67054d2175f18" }, { "children": [ "f1702d4ea25a11e7b7c67054d2175f18", "f1702e52a25a11e7b7c67054d2175f18", "f1702f60a25a11e7b7c67054d2175f18", "65289b36b97611e783eb3417ebd53aeb", "f1703064a25a11e7b7c67054d2175f18", "f170315ea25a11e7b7c67054d2175f18", "f1703262a25a11e7b7c67054d2175f18" ], "data": { "Name": "File Export" }, "format": "HEADINGS", "uid": "f1702c4aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "General Information", "Text": "Files are exported using the \"File > Export\" command. This will show a dialog box of available export types and options." }, "format": "HEAD_PARA", "uid": "f1702d4ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "HTML Export", "Text": "The HTML export subtypes can export to a single page or to multiple pages. The single page export contains all of the indented output. A navigation pane on the left with links to anchors at node positions is optional.
\n
\nThe multiple HTML pages export has one web page per node. It includes a navigation pane on the left with links to the pages with sibling, parent and aunt/uncle nodes.
\n
\nMultiple HTML data tables export 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.
\n
\nThe Live Tree HTML export uses Javascript and CSS to create an interactive view. Nodes in the tree can be expanded and collapsed, and a separate pane shows the output for all descendants of the selected node. The first form of this export creates separate files that are linked to the existing TreeLine file. These files are intended for use on a web server (browser security usually prevents local access). The HTML file stores the relative path from the TreeLine file to the initial export directory, so this relationship needs to be maintained on the web server. Only the TreeLine file needs to be replaced to update the data (re-export is not required).
\n
\nThe second form of Live Tree export creates a single file (with embedded data) that can be accessed from a local web browser. It appears the same as the first form on the browser." }, "format": "HEAD_PARA", "uid": "f1702e52a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Text Export", "Text": "Data can be exported to tabbed title text, comma-delimited (CSV) tables and tab-delimited tables. These formats are the same as the corresponding import formats.
\n
\nThere is also an unformatted text export the dumps all of the output into a text file without preserving the tree structure." }, "format": "HEAD_PARA", "uid": "f1702f60a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "XML Export", "Text": "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." }, "format": "HEAD_PARA", "uid": "f1703064a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "ODF Export", "Text": "TreeLine will export an outline to an Open Document Format (ODF) text document, compatible with Apache OpenOffice and LibreOffice. 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." }, "format": "HEAD_PARA", "uid": "f170315ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Bookmarks Export", "Text": "TreeLine will export bookmark files in both the Mozilla HTML format (Firefox browser) and the XBEL format (Konqueror, Galeon and Elinks browsers). TreeLine will look for a link field in each node that becomes the target of the bookmark." }, "format": "HEAD_PARA", "uid": "f1703262a25a11e7b7c67054d2175f18" }, { "children": [ "f1703442a25a11e7b7c67054d2175f18", "f1703546a25a11e7b7c67054d2175f18", "f170364aa25a11e7b7c67054d2175f18", "f1703744a25a11e7b7c67054d2175f18", "9ff09734dc9011ea887bac675dac20af", "f1703848a25a11e7b7c67054d2175f18", "7e9b3b88c24511e896c7d66a6ab671cb" ], "data": { "Name": "Customizations" }, "format": "HEADINGS", "uid": "f170335ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Options", "Text": "TreeLine's behavior can be modified with several settings available in \"Tools > General Options\". Most of these options are covered elsewhere in this document." }, "format": "HEAD_PARA", "uid": "f1703442a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Keyboard Shortcuts", "Text": "Keyboard shortcuts can be customized by using the \"Tools > Set Keyboard Shortcuts\" command. Simply type the new key sequence with the appropriate field selected." }, "format": "HEAD_PARA", "uid": "f1703546a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Toolbars", "Text": "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." }, "format": "HEAD_PARA", "uid": "f170364aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Fonts", "Text": "The default font used in the application can be set using the \"Tools > Customize Fonts\" menu. There are also options to specify fonts used in the tree views, the output view and the editor views." }, "format": "HEAD_PARA", "uid": "f1703744a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Tree Icons", "Text": "There is an icons directory in the user configuration directory (\"~/.treeline-x.x/icons\" on Linux, \"Users\\<user>\\AppData\\roaming\\bellz\\treeline-x.x\\icons\" on Windows). Image files (PNG or BMP) placed into this directory are available for use as tree icons." }, "format": "HEAD_PARA", "uid": "f1703848a25a11e7b7c67054d2175f18" }, { "children": [ "b5fe698830e111ebbc097054d2175f18", "7ffeb66ddc8711ea9a87ac675dac20af", "194b0533e5d811e9b5fea44cc8e97404", "db25cd62481e11e989f27054d2175f18", "800a0971305111e991cda44cc8e97404", "9c08f39ee80311e8a510a44cc8e97404", "6610203fd2c111e8b033d66a6ab671cb", "67e95a5bbbf611e891f7a44cc8e97404", "e8e10d33a14311e88e24a44cc8e97404", "4bc47248786711e8a8bfa44cc8e97404", "95a6e90a333f11e89efed66a6ab671cb", "6df9a0c5ba3b11e78e7e3417ebd53aeb", "f1703e7ea25a11e7b7c67054d2175f18", "f1704752a25a11e7b7c67054d2175f18", "f170527ea25a11e7b7c67054d2175f18", "f170652aa25a11e7b7c67054d2175f18", "f1706be2a25a11e7b7c67054d2175f18", "f170748ea25a11e7b7c67054d2175f18", "f1708032a25a11e7b7c67054d2175f18", "f1708ae6a25a11e7b7c67054d2175f18", "f170ab34a25a11e7b7c67054d2175f18", "f170c1aaa25a11e7b7c67054d2175f18", "f170e900a25a11e7b7c67054d2175f18", "f170fddca25a11e7b7c67054d2175f18", "f1710e58a25a11e7b7c67054d2175f18", "f1711f24a25a11e7b7c67054d2175f18" ], "data": { "Name": "Revision History" }, "format": "HEADINGS", "uid": "f1703d8ea25a11e7b7c67054d2175f18" }, { "children": [ "f1703f8ca25a11e7b7c67054d2175f18", "f170427aa25a11e7b7c67054d2175f18" ], "data": { "Name": "March 26, 2017 - Release 2.1.2 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f1703e7ea25a11e7b7c67054d2175f18" }, { "children": [ "f170407ca25a11e7b7c67054d2175f18", "f1704176a25a11e7b7c67054d2175f18" ], "data": { "Name": "Notes" }, "format": "BULLET_HEADING", "uid": "f1703f8ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Unstable snapshot", "Text": "This is an unstable development snapshot of TreeLine. It may contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 2.0.x) should probably be used for critical work." }, "format": "BULLETS", "uid": "f170407ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Translations", "Text": "The GUI and documentation translations have not yet been updated." }, "format": "BULLETS", "uid": "f1704176a25a11e7b7c67054d2175f18" }, { "children": [ "f1704360a25a11e7b7c67054d2175f18", "f170445aa25a11e7b7c67054d2175f18", "f1704554a25a11e7b7c67054d2175f18", "f170464ea25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f170427aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "DateTime output", "Text": "Fix problems formatting DateTime fields in output views." }, "format": "BULLETS", "uid": "f1704360a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Internal links", "Text": "Fix dialog issues with clicking on targets when creating inline internal links." }, "format": "BULLETS", "uid": "f170445aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Data view tabbing", "Text": "Fix a regression that prevented using the tab key to cycle between data edit view items." }, "format": "BULLETS", "uid": "f1704554a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Autosave restore", "Text": "Avoid opening two windows when restoring an auto-saved backup file." }, "format": "BULLETS", "uid": "f170464ea25a11e7b7c67054d2175f18" }, { "children": [ "f1704842a25a11e7b7c67054d2175f18", "f1704b30a25a11e7b7c67054d2175f18", "f1704d1aa25a11e7b7c67054d2175f18" ], "data": { "Name": "March 12, 2017 - Release 2.1.1 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f1704752a25a11e7b7c67054d2175f18" }, { "children": [ "f1704928a25a11e7b7c67054d2175f18", "f1704a2ca25a11e7b7c67054d2175f18" ], "data": { "Name": "Notes" }, "format": "BULLET_HEADING", "uid": "f1704842a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Unstable snapshot", "Text": "This is an unstable development snapshot of TreeLine. It may contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 2.0.x) should probably be used for critical work." }, "format": "BULLETS", "uid": "f1704928a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Translations", "Text": "The GUI and documentation translations have not yet been updated." }, "format": "BULLETS", "uid": "f1704a2ca25a11e7b7c67054d2175f18" }, { "children": [ "f1704c20a25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f1704b30a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Windows binary", "Text": "The Windows binary is now built using Python 3.6 and PyQt 5.8." }, "format": "BULLETS", "uid": "f1704c20a25a11e7b7c67054d2175f18" }, { "children": [ "f1704f54a25a11e7b7c67054d2175f18", "f170506ca25a11e7b7c67054d2175f18", "f1705166a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f1704d1aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Set icon crash", "Text": "Fix a crash when changing the icon on the Type Config page of the Configure Data Types dialog box." }, "format": "BULLETS", "uid": "f1704f54a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Type list issues", "Text": "Fix inconsistent type selections on the Type List page of the Configure Data Types dialog box." }, "format": "BULLETS", "uid": "f170506ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Window placement", "Text": "Fix issues with restoring the placement of the TreeLine window in some multiple monitor setups." }, "format": "BULLETS", "uid": "f1705166a25a11e7b7c67054d2175f18" }, { "children": [ "f170536ea25a11e7b7c67054d2175f18", "f170565ca25a11e7b7c67054d2175f18", "f170603ea25a11e7b7c67054d2175f18", "f1706228a25a11e7b7c67054d2175f18" ], "data": { "Name": "February 20, 2017 - Release 2.1.0 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f170527ea25a11e7b7c67054d2175f18" }, { "children": [ "f170545ea25a11e7b7c67054d2175f18", "f1705558a25a11e7b7c67054d2175f18" ], "data": { "Name": "Notes" }, "format": "BULLET_HEADING", "uid": "f170536ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Unstable snapshot", "Text": "This is an unstable development snapshot of TreeLine. It probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 2.0.x) should probably be used for critical work." }, "format": "BULLETS", "uid": "f170545ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Translations", "Text": "The GUI and documentation translations have not yet been updated." }, "format": "BULLETS", "uid": "f1705558a25a11e7b7c67054d2175f18" }, { "children": [ "f170574ca25a11e7b7c67054d2175f18", "f1705850a25a11e7b7c67054d2175f18", "f1705954a25a11e7b7c67054d2175f18", "f1705a4ea25a11e7b7c67054d2175f18", "f1705b48a25a11e7b7c67054d2175f18", "f1705c4ca25a11e7b7c67054d2175f18", "f1705d46a25a11e7b7c67054d2175f18", "f1705e40a25a11e7b7c67054d2175f18", "f1705f3aa25a11e7b7c67054d2175f18" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "f170565ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Port to Qt5", "Text": "TreeLine has been ported from the Qt4 library to the Qt5 library. The system requirements have been updated to Python 3.4 or higher, PyQt 5.4 or higher and Qt 5.4 or higher." }, "format": "BULLETS", "uid": "f170574ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Cloned nodes", "Text": "Cloned nodes that can duplicate sections of the tree have been added. Editing their data or child structure in one location changes all locations. They are created by copying nodes and then using the \"Edit->Paste Cloned Node\" command to paste them elsewhere. The clone link can be removed by deleting the nodes or by cutting them and pasting them back using the regular paste command." }, "format": "BULLETS", "uid": "f1705850a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "DateTime field", "Text": "Added a new DateTime field that combines dates and times into a single field that is useful for timestamps. The editor format is a combination of the date and time formats from the general options, separated by a space." }, "format": "BULLETS", "uid": "f1705954a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "HTML in choice fields", "Text": "Added an option to allow HTML tags in Choice, AutoChoice, Combination, AutoCombination and RegularExpression fields. The control is in the Field Config tab of the Configure Data Types dialog. This option is disabled by default. If enabled, special characters (angled brackets and quotation marks) will need to be escaped to show up in text." }, "format": "BULLETS", "uid": "f1705a4ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Saved conditional rules", "Text": "Saved rules for Conditional Find and Conditional Filter commands are listed directly in the dialog boxes, making them easier to load and save." }, "format": "BULLETS", "uid": "f1705b48a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "CSV import/export", "Text": "Added comma-delimited (CSV) table import and export, similar to existing tab-delimited table import and export. The export only handles data from a single level of child nodes." }, "format": "BULLETS", "uid": "f1705c4ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Incremental title search", "Text": "Added a quick, incremental search of node titles. By default, it's bound to ctrl+/. Then, matching titles are found as the search string is typed. The F3 and shift+F3 keys can be used to go to the next or previous matches, respectively." }, "format": "BULLETS", "uid": "f1705d46a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Multiple field selection", "Text": "Multiple fields can now be selected in the Output page of the Configuration dialog, so several fields can be added to the formats simultaneously." }, "format": "BULLETS", "uid": "f1705e40a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Print settings", "Text": "A pull-down selector for printers has been added to the Print Setup dialog. This enables TreeLine to verify that the page size and margin settings are supported by the current printer." }, "format": "BULLETS", "uid": "f1705f3aa25a11e7b7c67054d2175f18" }, { "children": [ "f170612ea25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f170603ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "SpacedText Field special characters", "Text": "The SpacedText Field now allows special HTML characters (angled brackets and quotation marks) without requiring them to be manually escaped." }, "format": "BULLETS", "uid": "f170612ea25a11e7b7c67054d2175f18" }, { "children": [ "f170630ea25a11e7b7c67054d2175f18", "f1706426a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f1706228a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Printed page splits", "Text": "The code that splits content into pages when printing has been improved. It now handles very long nodes better." }, "format": "BULLETS", "uid": "f170630ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Formatted field title edits", "Text": "When formatted fields (dates, times, etc.) are used in node titles, editing the titles in the tree or in the Title List pane will now work if the edit and output formats are similar." }, "format": "BULLETS", "uid": "f1706426a25a11e7b7c67054d2175f18" }, { "children": [ "f1706610a25a11e7b7c67054d2175f18", "f1706804a25a11e7b7c67054d2175f18" ], "data": { "Name": "October 3, 2015 - Release 2.0.2 (new stable release)" }, "format": "HEADINGS", "uid": "f170652aa25a11e7b7c67054d2175f18" }, { "children": [ "f1706700a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f1706610a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Data edit regression", "Text": "Fixed a major regression in 2.0.1 that broke data editors for most specialized field types (number, math, boolean, choice, etc.)" }, "format": "BULLETS", "uid": "f1706700a25a11e7b7c67054d2175f18" }, { "children": [ "f17068f4a25a11e7b7c67054d2175f18", "f17069e4a25a11e7b7c67054d2175f18", "f1706ae8a25a11e7b7c67054d2175f18" ], "data": { "Name": "Compatibility Notes" }, "format": "BULLET_HEADING", "uid": "f1706804a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File format", "Text": "There are some file format changes between TreeLine 1.4.x and this version of TreeLine." }, "format": "BULLETS", "uid": "f17068f4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File conversion", "Text": "Older files opened in this version are automatically converted when saved." }, "format": "BULLETS", "uid": "f17069e4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File compatibility", "Text": "Files saved in this version may not be fully compatible with TreeLine 1.4.x." }, "format": "BULLETS", "uid": "f1706ae8a25a11e7b7c67054d2175f18" }, { "children": [ "f1706cd2a25a11e7b7c67054d2175f18", "f1706fb6a25a11e7b7c67054d2175f18" ], "data": { "Name": "September 26, 2015 - Release 2.0.1 (new stable release)" }, "format": "HEADINGS", "uid": "f1706be2a25a11e7b7c67054d2175f18" }, { "children": [ "f1706db8a25a11e7b7c67054d2175f18", "f1706ebca25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f1706cd2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Plugin options", "Text": "Added methods to the plugin interface that allow general program options to be queried and changed." }, "format": "BULLETS", "uid": "f1706db8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Table import errors", "Text": "Improve text table import error messages by including the line number where the problem is found." }, "format": "BULLETS", "uid": "f1706ebca25a11e7b7c67054d2175f18" }, { "children": [ "f170709ca25a11e7b7c67054d2175f18", "f1707196a25a11e7b7c67054d2175f18", "f1707290a25a11e7b7c67054d2175f18", "f1707380a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f1706fb6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Data edit undo", "Text": "Reduce the amount of work that a single undo command removes from editors in the data edit view." }, "format": "BULLETS", "uid": "f170709ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Configuration changes", "Text": "Fixed a bug that prevented setting the unique ID reference field on a newly created data type." }, "format": "BULLETS", "uid": "f1707196a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Legacy newline convert", "Text": "Preserve hard newlines in text fields when converting TreeLine 1.4.x files to this version." }, "format": "BULLETS", "uid": "f1707290a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Indent expand status", "Text": "Fix problems preserving expand/collapse node states when indenting and unindenting nodes." }, "format": "BULLETS", "uid": "f1707380a25a11e7b7c67054d2175f18" }, { "children": [ "f1707592a25a11e7b7c67054d2175f18", "f1707c5ea25a11e7b7c67054d2175f18" ], "data": { "Name": "May 17, 2015 - Release 2.0.0 (new stable release)" }, "format": "HEADINGS", "uid": "f170748ea25a11e7b7c67054d2175f18" }, { "children": [ "f1707682a25a11e7b7c67054d2175f18", "f170777ca25a11e7b7c67054d2175f18", "f1707876a25a11e7b7c67054d2175f18", "f1707966a25a11e7b7c67054d2175f18", "f1707a60a25a11e7b7c67054d2175f18", "f1707b5aa25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f1707592a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Treepad import update", "Text": "Modified the Treepad file import to use SpacedText fields to more closely match Treepad formatting." }, "format": "BULLETS", "uid": "f1707682a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Treepad export", "Text": "An optional plugin was written that can export files to the Treepad text file format." }, "format": "BULLETS", "uid": "f170777ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "New icon", "Text": "The TreeLine icon was replaced with a new one. Thanks to David Reimer for contributing the artwork." }, "format": "BULLETS", "uid": "f1707876a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Translation updates", "Text": "The German and Portuguese GUI translations were updated." }, "format": "BULLETS", "uid": "f1707966a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Sample file updates", "Text": "Updated the long text sample file to include the SpacedText field type, and added a conditional equation to the math sample file." }, "format": "BULLETS", "uid": "f1707a60a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Documentation update", "Text": "Updated the Math Field section of the documentation." }, "format": "BULLETS", "uid": "f1707b5aa25a11e7b7c67054d2175f18" }, { "children": [ "f1707d44a25a11e7b7c67054d2175f18", "f1707e3ea25a11e7b7c67054d2175f18", "f1707f38a25a11e7b7c67054d2175f18" ], "data": { "Name": "Compatibility Notes" }, "format": "BULLET_HEADING", "uid": "f1707c5ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File format", "Text": "There are some file format changes between TreeLine 1.4.x and this version of TreeLine." }, "format": "BULLETS", "uid": "f1707d44a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File conversion", "Text": "Older files opened in this version are automatically converted when saved." }, "format": "BULLETS", "uid": "f1707e3ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File compatibility", "Text": "Files saved in this version may not be fully compatible with TreeLine 1.4.x." }, "format": "BULLETS", "uid": "f1707f38a25a11e7b7c67054d2175f18" }, { "children": [ "f1708118a25a11e7b7c67054d2175f18", "f17083fca25a11e7b7c67054d2175f18", "f1708802a25a11e7b7c67054d2175f18" ], "data": { "Name": "March 29, 2015 - Release 1.9.7 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f1708032a25a11e7b7c67054d2175f18" }, { "children": [ "f1708208a25a11e7b7c67054d2175f18", "f1708302a25a11e7b7c67054d2175f18" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "f1708118a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Math field conditions", "Text": "Added comparison operators and conditional if statements to math field equations. The operators can be used with a new boolean result type, or as a part of numeric, date, time or text expressions." }, "format": "BULLETS", "uid": "f1708208a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Math field text operations", "Text": "Text operators were made available in math field equations, and the result type can be set to text. This allows math fields to combine text from other fields, replace sub-strings and change capitalization." }, "format": "BULLETS", "uid": "f1708302a25a11e7b7c67054d2175f18" }, { "children": [ "f17084eca25a11e7b7c67054d2175f18", "f17085f0a25a11e7b7c67054d2175f18", "f17086fea25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f17083fca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Saved status bar message", "Text": "Added a \"file saved\" status bar message." }, "format": "BULLETS", "uid": "f17084eca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "German translation", "Text": "Added a German GUI translation." }, "format": "BULLETS", "uid": "f17085f0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Portuguese translation", "Text": "Updated the Portuguese GUI translation." }, "format": "BULLETS", "uid": "f17086fea25a11e7b7c67054d2175f18" }, { "children": [ "f17088e8a25a11e7b7c67054d2175f18", "f17089e2a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f1708802a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Combination editor", "Text": "Fixed a focus problem that made Combination and AutoCombination field editor pull-downs unusable." }, "format": "BULLETS", "uid": "f17088e8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Choice editor", "Text": "Fixed a focus problem that made the pull-downs in choice and boolean field editors unusable on Linux." }, "format": "BULLETS", "uid": "f17089e2a25a11e7b7c67054d2175f18" }, { "children": [ "f1708bcca25a11e7b7c67054d2175f18", "f17097aca25a11e7b7c67054d2175f18", "f170a2eca25a11e7b7c67054d2175f18" ], "data": { "Name": "March 10, 2015 - Release 1.9.6 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f1708ae6a25a11e7b7c67054d2175f18" }, { "children": [ "f1708cbca25a11e7b7c67054d2175f18", "f1708db6a25a11e7b7c67054d2175f18", "f1708ea6a25a11e7b7c67054d2175f18", "f1708faaa25a11e7b7c67054d2175f18", "f17090aea25a11e7b7c67054d2175f18", "f17091e4a25a11e7b7c67054d2175f18", "f17092dea25a11e7b7c67054d2175f18", "f1709644a25a11e7b7c67054d2175f18" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "f1708bcca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "OneLineText field", "Text": "Added a new OneLineText field type that restricts the text length to a single line." }, "format": "BULLETS", "uid": "f1708cbca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "All Types conditional test", "Text": "Added an All Types option to the conditional find and filter commands. This allows multiple node types to be found or filtered at the same time. The fields from every type are available for use in conditions. The conditions give a false result for all node types that do not contain that field name." }, "format": "BULLETS", "uid": "f1708db6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Math field dates and times", "Text": "Added support for date and time calculations using math fields. Math equation result types can be set to numeric, date or time output. Date fields can be subtracted to give the number of days elapsed, and numbers of days can be added to or subtracted from dates, resulting in new dates. Time fields can be subtracted to give the number of seconds elapsed, and numbers of seconds can be added to or subtracted from times, resulting in new times." }, "format": "BULLETS", "uid": "f1708ea6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Math field root references", "Text": "In math fields, added an equation reference level to reference fields in the root node. This provides a place for \"constant\" field values that can be referenced from any node but only need to be changed in one location." }, "format": "BULLETS", "uid": "f1708faaa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Plugin interface", "Text": "Support was added for plugin extension modules. Most of the interface methods from TreeLine 1.4.x were duplicated to ease porting of old plugins. Of course, old plugins must be ported from Python 2.x to Python 3.x, and there are multi-window implementation differences. New interfaces allow the creation of new field types and the execution of any menu command. Sample plugins are available on the TreeLine download page." }, "format": "BULLETS", "uid": "f17090aea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Automatic HTML tags", "Text": "Added text formatting and link commands for HTML fields that add tags to the HTML content." }, "format": "BULLETS", "uid": "f17091e4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Open in folder", "Text": "An open in folder command was added to external link field editors to open the directory in a file manager." }, "format": "BULLETS", "uid": "f17092dea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Select All command", "Text": "Added the Select All command to global menus with a different default shortcut key (ctrl-L) to avoid conflicts with the add child shortcut." }, "format": "BULLETS", "uid": "f1709644a25a11e7b7c67054d2175f18" }, { "children": [ "f170989ca25a11e7b7c67054d2175f18", "f170998ca25a11e7b7c67054d2175f18", "f1709a7ca25a11e7b7c67054d2175f18", "f1709b8aa25a11e7b7c67054d2175f18", "f1709c7aa25a11e7b7c67054d2175f18", "f1709d6aa25a11e7b7c67054d2175f18", "f1709e50a25a11e7b7c67054d2175f18", "f1709f40a25a11e7b7c67054d2175f18", "f170a026a25a11e7b7c67054d2175f18", "f170a116a25a11e7b7c67054d2175f18", "f170a206a25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f17097aca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Data edit view focus", "Text": "Improved the focus handling for data edit view edit boxes. This eliminates the blue outline for boxes in inactive data edit views. It also makes tab-to-focus more predictable, including fully selecting single-line field types when they receive tab focus." }, "format": "BULLETS", "uid": "f170989ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Data editor resize", "Text": "Data edit boxes are now automatically resized when editing is complete and the focus moves to another row." }, "format": "BULLETS", "uid": "f170998ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Drag external links", "Text": "Allow files to be drag & dropped on data edit boxes to create external links." }, "format": "BULLETS", "uid": "f1709a7ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Stay-on-top dialogs", "Text": "Small, non-modal dialogs, such as those for sorting, numbering, finding and filtering, have been set to stay on top, so they won't be obscured by TreeLine windows." }, "format": "BULLETS", "uid": "f1709b8aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Opening external links", "Text": "Made opening associated programs from external file links more consistent, especially in Linux." }, "format": "BULLETS", "uid": "f1709c7aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Copy types from file", "Text": "The copy types from file command now supports encrypted and compressed files." }, "format": "BULLETS", "uid": "f1709d6aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Wait cursors", "Text": "Added wait cursors to TreeLine operations that could be time consuming." }, "format": "BULLETS", "uid": "f1709e50a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Context menus", "Text": "Improved the consistency of context menus and shortcut commands used in edit boxes." }, "format": "BULLETS", "uid": "f1709f40a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Portuguese translation", "Text": "Added a nearly complete Portuguese GUI translation." }, "format": "BULLETS", "uid": "f170a026a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Sample file languages", "Text": "Added support for sample TreeLine files to be provided in alternate languages." }, "format": "BULLETS", "uid": "f170a116a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Documentation translation", "Text": "Prepared the help file for translation into other languages," }, "format": "BULLETS", "uid": "f170a206a25a11e7b7c67054d2175f18" }, { "children": [ "f170a3d2a25a11e7b7c67054d2175f18", "f170a4b8a25a11e7b7c67054d2175f18", "f170a5a8a25a11e7b7c67054d2175f18", "f170a698a25a11e7b7c67054d2175f18", "f170a77ea25a11e7b7c67054d2175f18", "f170a86ea25a11e7b7c67054d2175f18", "f170a95ea25a11e7b7c67054d2175f18", "f170aa44a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f170a2eca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Duplicate unique IDs", "Text": "Corrupted TreeLine files with the same unique ID assigned to multiple nodes no longer fail to open. The user is warned that unique IDs have been updated, which could break some internal links." }, "format": "BULLETS", "uid": "f170a3d2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Conditional field list", "Text": "Fixed missing fields in the pull-down list in the conditional find and filter dialog boxes when a rule was re-used after a node type change. " }, "format": "BULLETS", "uid": "f170a4b8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Math field missing references", "Text": "Fixed problems with math fields that reference non-existing fields in parent or child nodes." }, "format": "BULLETS", "uid": "f170a5a8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Math field type updates", "Text": "Made math fields update properly after node type changes." }, "format": "BULLETS", "uid": "f170a698a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Rename bullet/table fields", "Text": "Fixed inconsistent updates after renaming fields used with bulleted or tabled output." }, "format": "BULLETS", "uid": "f170a77ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Copying descendants", "Text": "Made node copy-paste and drag-and-drop work when the initial selection includes both parent and child nodes. " }, "format": "BULLETS", "uid": "f170a86ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Tabbed text import", "Text": "When importing a tabbed text file with multiple top-level nodes, create a single higher-level node to prevent failure." }, "format": "BULLETS", "uid": "f170a95ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Custom toolbar dialog", "Text": "Fixed the availability of the move up button in the customize toolbar dialog when the second command in the list is selected." }, "format": "BULLETS", "uid": "f170aa44a25a11e7b7c67054d2175f18" }, { "children": [ "f170ac38a25a11e7b7c67054d2175f18", "f170afeea25a11e7b7c67054d2175f18", "f170b836a25a11e7b7c67054d2175f18" ], "data": { "Name": "December 31, 2014 - Release 1.9.5 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f170ab34a25a11e7b7c67054d2175f18" }, { "children": [ "f170ad1ea25a11e7b7c67054d2175f18", "f170ae0ea25a11e7b7c67054d2175f18", "f170aefea25a11e7b7c67054d2175f18" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "f170ac38a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Math field type", "Text": "Added a math field type that is configured by defining an equation. The field value is automatically calculated based on references to numerical values in other nodes. See the \"sample_math_fields\" file for a usage example." }, "format": "BULLETS", "uid": "f170ad1ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Clear formatting", "Text": "Added a Clear Formatting command that removes font changes and links from data editor text." }, "format": "BULLETS", "uid": "f170ae0ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "GUI Translations", "Text": "Made source code changes necessary to support user interface translations into other languages. The actual translation work remains to be done." }, "format": "BULLETS", "uid": "f170aefea25a11e7b7c67054d2175f18" }, { "children": [ "f170b0caa25a11e7b7c67054d2175f18", "f170b1baa25a11e7b7c67054d2175f18", "f170b2aaa25a11e7b7c67054d2175f18", "f170b390a25a11e7b7c67054d2175f18", "f170b480a25a11e7b7c67054d2175f18", "f170b570a25a11e7b7c67054d2175f18", "f170b656a25a11e7b7c67054d2175f18", "f170b746a25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f170afeea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Combination field scroll bars", "Text": "Add scroll bars to the pull-down editors for combination fields with many entries." }, "format": "BULLETS", "uid": "f170b0caa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "External link truncation", "Text": "Reduce the truncation of external link URLs when generating default display names." }, "format": "BULLETS", "uid": "f170b1baa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "File newlines", "Text": "Use Unix-style newlines for saved TreeLine files to keep files consistent across platforms." }, "format": "BULLETS", "uid": "f170b2aaa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "HTML panes", "Text": "Update the CSS code in exported HTML with navigation panes to improve the appearance in some browsers." }, "format": "BULLETS", "uid": "f170b390a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Undo optimization", "Text": "Optimize some undo information to reduce the amount of data in memory." }, "format": "BULLETS", "uid": "f170b480a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Move sample files", "Text": "Move sample files into a separate directory to avoid future translation conflicts." }, "format": "BULLETS", "uid": "f170b570a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Installer clarification", "Text": "Clarify a Linux installer error message when checking for the Python 3 version of PyQt." }, "format": "BULLETS", "uid": "f170b656a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Include MSVC files", "Text": "Include MSVCR DLL files in the Windows installer." }, "format": "BULLETS", "uid": "f170b746a25a11e7b7c67054d2175f18" }, { "children": [ "f170b91ca25a11e7b7c67054d2175f18", "f170ba0ca25a11e7b7c67054d2175f18", "f170baf2a25a11e7b7c67054d2175f18", "f170bbeca25a11e7b7c67054d2175f18", "f170bcf0a25a11e7b7c67054d2175f18", "f170bde0a25a11e7b7c67054d2175f18", "f170bed0a25a11e7b7c67054d2175f18", "f170bfc0a25a11e7b7c67054d2175f18", "f170c0a6a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f170b836a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Applying config dialog settings", "Text": "Fix problems applying multiple configuration changes while the Configure Data Types dialog box remains open." }, "format": "BULLETS", "uid": "f170b91ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Title list line split", "Text": "In the Title List editor, splitting a title into two lines now creates a new node without losing the children and parameters of the original node." }, "format": "BULLETS", "uid": "f170ba0ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Title list undo", "Text": "Fix the undo command in the Title List View so that deleted lines/nodes are properly restored. " }, "format": "BULLETS", "uid": "f170baf2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Sorting by fields", "Text": "When sorting nodes by fields, properly handle a reverse direction." }, "format": "BULLETS", "uid": "f170bbeca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Find and replace", "Text": "Fix problems with the find and replace command when a particular node type is specified." }, "format": "BULLETS", "uid": "f170bcf0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Conditional types", "Text": "Fix problems defining conditional types from the Configure Data Types dialog box." }, "format": "BULLETS", "uid": "f170bde0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Apply font sizes", "Text": "Correctly apply font size formatting to selections with mixed font sizes." }, "format": "BULLETS", "uid": "f170bed0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Editor height", "Text": "Fix the height of long text field editors with customized data editor fonts." }, "format": "BULLETS", "uid": "f170bfc0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Base 64 image export", "Text": "Fix HTML export of Base 64 images." }, "format": "BULLETS", "uid": "f170c0a6a25a11e7b7c67054d2175f18" }, { "children": [ "f170c286a25a11e7b7c67054d2175f18", "f170e068a25a11e7b7c67054d2175f18", "f170e46ea25a11e7b7c67054d2175f18" ], "data": { "Name": "March 8, 2014 - Release 1.9.4 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f170c1aaa25a11e7b7c67054d2175f18" }, { "children": [ "f170d8d4a25a11e7b7c67054d2175f18", "f170d9d8a25a11e7b7c67054d2175f18", "f170dac8a25a11e7b7c67054d2175f18", "f170dbb8a25a11e7b7c67054d2175f18", "f170dca8a25a11e7b7c67054d2175f18", "f170dd98a25a11e7b7c67054d2175f18", "f170de88a25a11e7b7c67054d2175f18", "f170df6ea25a11e7b7c67054d2175f18" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "f170c286a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Documentation", "Text": "Created new documentation, including a TreeLine file with details and a text file with basic usage instructions. Both are accessible from the help menu." }, "format": "BULLETS", "uid": "f170d8d4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Font settings", "Text": "Added customizing of default fonts used in the tree, output and editor views." }, "format": "BULLETS", "uid": "f170d9d8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Spaced text field", "Text": "Added a new SpacedText field type that holds plain text and preserves all spacing." }, "format": "BULLETS", "uid": "f170dac8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Combination editor", "Text": "Combination and auto combination field types now use a simpler checkbox style pull-down editor." }, "format": "BULLETS", "uid": "f170dbb8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Autosave option", "Text": "An autosave option was added." }, "format": "BULLETS", "uid": "f170dca8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Unique ID output", "Text": "A unique ID reference field was added to the file info fields to allow node unique IDs to be included in output formats." }, "format": "BULLETS", "uid": "f170dd98a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Paste plain text", "Text": "A plain text paste command was added to paste non-formatted text to data editors." }, "format": "BULLETS", "uid": "f170de88a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Indent option", "Text": "Added an option to set the child indent offset amount." }, "format": "BULLETS", "uid": "f170df6ea25a11e7b7c67054d2175f18" }, { "children": [ "f170e162a25a11e7b7c67054d2175f18", "f170e252a25a11e7b7c67054d2175f18", "f170e36aa25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f170e068a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Print preview size", "Text": "The last size and position of the print preview window are remembered and restored." }, "format": "BULLETS", "uid": "f170e162a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Page breaks in nodes", "Text": "When printing, nodes with long text content are split between pages." }, "format": "BULLETS", "uid": "f170e252a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Export relative links", "Text": "In multiple page HTML exports, relative links in the content are adjusted based on the directory depth." }, "format": "BULLETS", "uid": "f170e36aa25a11e7b7c67054d2175f18" }, { "children": [ "f170e54aa25a11e7b7c67054d2175f18", "f170e63aa25a11e7b7c67054d2175f18", "f170e720a25a11e7b7c67054d2175f18", "f170e810a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f170e46ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Blank nodes", "Text": "Fixed problems outputting completely blank nodes." }, "format": "BULLETS", "uid": "f170e54aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Conditional rules", "Text": "Problems with the contains and true/false conditional rules were fixed." }, "format": "BULLETS", "uid": "f170e63aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Link dialogs", "Text": "Fixed issues displaying several editor link dialogs in quick succession." }, "format": "BULLETS", "uid": "f170e720a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Output config cursor", "Text": "In the configure data types dialog, the output format cursor no longer moves when switching to other field references." }, "format": "BULLETS", "uid": "f170e810a25a11e7b7c67054d2175f18" }, { "children": [ "f170e9dca25a11e7b7c67054d2175f18", "f170f12aa25a11e7b7c67054d2175f18", "f170f846a25a11e7b7c67054d2175f18" ], "data": { "Name": "January 19, 2014 - Release 1.9.3 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f170e900a25a11e7b7c67054d2175f18" }, { "children": [ "f170eac2a25a11e7b7c67054d2175f18", "f170eba8a25a11e7b7c67054d2175f18", "f170ec8ea25a11e7b7c67054d2175f18", "f170ed74a25a11e7b7c67054d2175f18", "f170ee5aa25a11e7b7c67054d2175f18", "f170ef4aa25a11e7b7c67054d2175f18", "f170f030a25a11e7b7c67054d2175f18" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "f170e9dca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Keyboard shortcuts", "Text": "Added controls in the Tools menu for customizing TreeLine's keyboard shortcuts." }, "format": "BULLETS", "uid": "f170eac2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Toolbars", "Text": "Controls for customizing TreeLine's toolbar buttons were added to the Tools menu." }, "format": "BULLETS", "uid": "f170eba8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Conditional search", "Text": "New dialogs were created for conditional finding and filtering of nodes. Specific conditions can be applied to individual types and fields, and the conditions can be saved." }, "format": "BULLETS", "uid": "f170ec8ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Find and replace", "Text": "Find and replace functionality was added to search and change nodes' text data. The search can be limited to specific types and fields." }, "format": "BULLETS", "uid": "f170ed74a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Table output", "Text": "A new option for output data in tables was added to the Type Config pane of the Configure Data Types dialog. Each line of the output format becomes a column. Any text at the start of the format line that is followed by a colon becomes a table heading." }, "format": "BULLETS", "uid": "f170ee5aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Bullet output", "Text": "An option to add bullets to the output of child nodes was added to the Type Config pane of the Configure Data Types dialog." }, "format": "BULLETS", "uid": "f170ef4aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Toggle selections", "Text": "Added View > Previous Selection and Next Selection commands to step through the node selection history." }, "format": "BULLETS", "uid": "f170f030a25a11e7b7c67054d2175f18" }, { "children": [ "f170f210a25a11e7b7c67054d2175f18", "f170f2f6a25a11e7b7c67054d2175f18", "f170f418a25a11e7b7c67054d2175f18", "f170f4fea25a11e7b7c67054d2175f18", "f170f5e4a25a11e7b7c67054d2175f18", "f170f6d4a25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f170f12aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Drop to reorder", "Text": "Nodes can now be reordered by dragging and dropping them between sibling nodes." }, "format": "BULLETS", "uid": "f170f210a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Date entry formats", "Text": "The keyboard entry of dates and times into fields was made more flexible by allowing entries such as 4-digit years that don't exactly match the entry format." }, "format": "BULLETS", "uid": "f170f2f6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Default link names", "Text": "Any text selection is now used as the default name for links inserted into text fields." }, "format": "BULLETS", "uid": "f170f418a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Anchor links", "Text": "Links to local named anchors in a node's HTML text content now work if they don't conflict with any node unique IDs." }, "format": "BULLETS", "uid": "f170f4fea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Line spacing", "Text": "Line spacing in output views was made more consistent." }, "format": "BULLETS", "uid": "f170f5e4a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Saved states", "Text": "Improved the efficiency of restoring node open/close states when opening files." }, "format": "BULLETS", "uid": "f170f6d4a25a11e7b7c67054d2175f18" }, { "children": [ "f170f936a25a11e7b7c67054d2175f18", "f170fa1ca25a11e7b7c67054d2175f18", "f170fb0ca25a11e7b7c67054d2175f18", "f170fbf2a25a11e7b7c67054d2175f18", "f170fce2a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f170f846a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Paste font sizes", "Text": "Errors when pasting text with varying font sizes into data editors were fixed." }, "format": "BULLETS", "uid": "f170f936a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Conditional data types", "Text": "Various issues with conditional data types were fixed, including problems with pasting conditional nodes and prompt updating when the types change." }, "format": "BULLETS", "uid": "f170fa1ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Drag undo", "Text": "Problems with undoing the dragging and dropping of a node were fixed." }, "format": "BULLETS", "uid": "f170fb0ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Expand branches", "Text": "Fix the extremely slow operation of the View > Expand Full Branch and Collapse Full Branch commands." }, "format": "BULLETS", "uid": "f170fbf2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Printed lines", "Text": "Fix improperly placed branch lines in printed output." }, "format": "BULLETS", "uid": "f170fce2a25a11e7b7c67054d2175f18" }, { "children": [ "f170fec2a25a11e7b7c67054d2175f18", "f171053ea25a11e7b7c67054d2175f18", "f17108e0a25a11e7b7c67054d2175f18" ], "data": { "Name": "October 22, 2013 - Release 1.9.2 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f170fddca25a11e7b7c67054d2175f18" }, { "children": [ "f170ffa8a25a11e7b7c67054d2175f18", "f171008ea25a11e7b7c67054d2175f18", "f171017ea25a11e7b7c67054d2175f18", "f1710264a25a11e7b7c67054d2175f18", "f171034aa25a11e7b7c67054d2175f18", "f1710430a25a11e7b7c67054d2175f18" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "f170fec2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Spell check", "Text": "Added a spell check tool. This requires either aspell, ispell or hunspell to be installed." }, "format": "BULLETS", "uid": "f170ffa8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Numbering field type", "Text": "Included a node numbering field type with several formatting options. An update numbering command fills in the sequence." }, "format": "BULLETS", "uid": "f171008ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Add category level", "Text": "Added a command to add a category level based on a subset of data fields." }, "format": "BULLETS", "uid": "f171017ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Flatten by category", "Text": "Added a flatten by category command to combine parent fields into child nodes." }, "format": "BULLETS", "uid": "f1710264a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Flatten by link", "Text": "A new flatten by link command flattens the structure and provides internal links to the former parent nodes." }, "format": "BULLETS", "uid": "f171034aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Arrange by link", "Text": "An arrange by link command restores the structure based on parent internal links." }, "format": "BULLETS", "uid": "f1710430a25a11e7b7c67054d2175f18" }, { "children": [ "f171061aa25a11e7b7c67054d2175f18", "f171070aa25a11e7b7c67054d2175f18", "f17107f0a25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f171053ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "URL drag-and-drop", "Text": "Allow file URL drag-and-drop on active external link data edit widgets." }, "format": "BULLETS", "uid": "f171061aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Share directory", "Text": "Change the Linux installer to use the 'share' directory in place of 'lib' for python files." }, "format": "BULLETS", "uid": "f171070aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Windows binary", "Text": "Update the Windows binary to use version 3.3 of Python." }, "format": "BULLETS", "uid": "f17107f0a25a11e7b7c67054d2175f18" }, { "children": [ "f17109bca25a11e7b7c67054d2175f18", "f1710aa2a25a11e7b7c67054d2175f18", "f1710b88a25a11e7b7c67054d2175f18", "f1710c78a25a11e7b7c67054d2175f18", "f1710d68a25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f17108e0a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Output line sequence", "Text": "Fix out of sequence output lines when output formats are longer than ten lines." }, "format": "BULLETS", "uid": "f17109bca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Node indenting", "Text": "Fix problems with unique IDs and internal links when indenting and unindenting nodes." }, "format": "BULLETS", "uid": "f1710aa2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Widget focus problems", "Text": "Avoid widget focus problems when editing data on conditional types." }, "format": "BULLETS", "uid": "f1710b88a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Format cursor position", "Text": "Maintain the output format cursor position when changing fields in the configure dialog." }, "format": "BULLETS", "uid": "f1710c78a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "XML import", "Text": "Fix importing of generic XML documents that have nodes with no data." }, "format": "BULLETS", "uid": "f1710d68a25a11e7b7c67054d2175f18" }, { "children": [ "f1710f48a25a11e7b7c67054d2175f18", "f17118a8a25a11e7b7c67054d2175f18" ], "data": { "Name": "May 2, 2013 - Release 1.9.1 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f1710e58a25a11e7b7c67054d2175f18" }, { "children": [ "f171102ea25a11e7b7c67054d2175f18", "f171111ea25a11e7b7c67054d2175f18", "f171120ea25a11e7b7c67054d2175f18", "f17112fea25a11e7b7c67054d2175f18", "f17113eea25a11e7b7c67054d2175f18", "f17114dea25a11e7b7c67054d2175f18", "f17115d8a25a11e7b7c67054d2175f18", "f17116c8a25a11e7b7c67054d2175f18", "f17117b8a25a11e7b7c67054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "f1710f48a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Compression and encryption", "Text": "Added TreeLine file compression and file encryption, controlled from a File > Properties dialog box." }, "format": "BULLETS", "uid": "f171102ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Restore tree states", "Text": "Tree node open/close states are restored for recent files." }, "format": "BULLETS", "uid": "f171111ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Derived data types", "Text": "Added derived data types that keep the field list of their generic type." }, "format": "BULLETS", "uid": "f171120ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Conditional type setting", "Text": "Added conditional type setting that changes icons or output format based on field contents." }, "format": "BULLETS", "uid": "f17112fea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Other field references", "Text": "Other field references (file info, ancestors, children) can be used in node output formats and in print headers & footers." }, "format": "BULLETS", "uid": "f17113eea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "HTML file export", "Text": "Added an HTML file export to a single file with a navigation pane on the side." }, "format": "BULLETS", "uid": "f17114dea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Embedded blank lines", "Text": "Allow embedded blank lines in non-HTML node output formats." }, "format": "BULLETS", "uid": "f17115d8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Separator config option", "Text": "Added an output separator config option for combination fields and child references." }, "format": "BULLETS", "uid": "f17116c8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "First day of week option", "Text": "Add an option to set the first day of the week for calendar widgets." }, "format": "BULLETS", "uid": "f17117b8a25a11e7b7c67054d2175f18" }, { "children": [ "f1711998a25a11e7b7c67054d2175f18", "f1711a88a25a11e7b7c67054d2175f18", "f1711b6ea25a11e7b7c67054d2175f18", "f1711c54a25a11e7b7c67054d2175f18", "f1711d44a25a11e7b7c67054d2175f18", "f1711e2aa25a11e7b7c67054d2175f18" ], "data": { "Name": "Bug Fixes" }, "format": "BULLET_HEADING", "uid": "f17118a8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Drag multiple nodes", "Text": "Fixed problems with pasting or dragging multiple nodes." }, "format": "BULLETS", "uid": "f1711998a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Remove fields from multi-line output formats", "Text": "Made removing fields from multi-line output formats work properly." }, "format": "BULLETS", "uid": "f1711a88a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Prevent duplicate unique IDs", "Text": "Prevent duplicate unique IDs from being created after undoing the deletion of a branch." }, "format": "BULLETS", "uid": "f1711b6ea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Fixed non-text nodes sorting", "Text": "Fixed node sorting of non-text nodes (numbers, dates, times, etc.)" }, "format": "BULLETS", "uid": "f1711c54a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Calendar widgets", "Text": "Avoid placing calendar widgets partially off screen if near the bottom." }, "format": "BULLETS", "uid": "f1711d44a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Automatic cell height", "Text": "Limit the automatic height increases for text edit cells to avoid confusing double scroll bars." }, "format": "BULLETS", "uid": "f1711e2aa25a11e7b7c67054d2175f18" }, { "children": [ "f171200aa25a11e7b7c67054d2175f18" ], "data": { "Name": "February 6, 2013 - Release 1.9.0 (unstable development snapshot)" }, "format": "HEADINGS", "uid": "f1711f24a25a11e7b7c67054d2175f18" }, { "children": [ "f17120e6a25a11e7b7c67054d2175f18", "f17121d6a25a11e7b7c67054d2175f18", "f17122c6a25a11e7b7c67054d2175f18", "f17123aca25a11e7b7c67054d2175f18", "f171249ca25a11e7b7c67054d2175f18", "f171258ca25a11e7b7c67054d2175f18", "f1712690a25a11e7b7c67054d2175f18", "f1712776a25a11e7b7c67054d2175f18", "f1712866a25a11e7b7c67054d2175f18", "f1712960a25a11e7b7c67054d2175f18", "f1712a46a25a11e7b7c67054d2175f18", "f1712b36a25a11e7b7c67054d2175f18", "f1712c1ca25a11e7b7c67054d2175f18", "f1712d02a25a11e7b7c67054d2175f18", "f1712df2a25a11e7b7c67054d2175f18", "f1712ed8a25a11e7b7c67054d2175f18", "f1712fbea25a11e7b7c67054d2175f18", "f1713194a25a11e7b7c67054d2175f18", "f1713284a25a11e7b7c67054d2175f18", "f171336aa25a11e7b7c67054d2175f18" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "f171200aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Full Python 3 rewrite", "Text": "TreeLine has been fully rewritten using Python 3." }, "format": "BULLETS", "uid": "f17120e6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Direct model-view and ElementTree", "Text": "Improved performance due to direct use of model-view classes for views and ElementTree for input/output." }, "format": "BULLETS", "uid": "f17121d6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Table based data editor pane", "Text": "A table based data editor pane (much faster)." }, "format": "BULLETS", "uid": "f17122c6a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "WYSIWYG data editor view.", "Text": "WYSIWYG formatting in the data editor view." }, "format": "BULLETS", "uid": "f17123aca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Real-time window updates ", "Text": "Real-time updates of the same file shown in multiple windows." }, "format": "BULLETS", "uid": "f171249ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Improved printing", "Text": "Improved printing and print preview performance." }, "format": "BULLETS", "uid": "f171258ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Saves print options", "Text": "Saves print options with the TreeLine file." }, "format": "BULLETS", "uid": "f1712690a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Built-in PDF", "Text": "Built-in print to PDF function." }, "format": "BULLETS", "uid": "f1712776a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Searching options.", "Text": "More searching options." }, "format": "BULLETS", "uid": "f1712866a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Filtering command", "Text": "A filtering command shows matches in a simple list." }, "format": "BULLETS", "uid": "f1712960a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Saved sorting parameters", "Text": "Sorting parameters can be saved with each data type." }, "format": "BULLETS", "uid": "f1712a46a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Basic text field type", "Text": "The basic text field type allows formatting, preserves line breaks and allows HTML restricted characters." }, "format": "BULLETS", "uid": "f1712b36a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Calendar widget", "Text": "A calendar widget can be used for editing date fields." }, "format": "BULLETS", "uid": "f1712c1ca25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Easy internal links", "Text": "Internal link fields and inline internal links are easier to use." }, "format": "BULLETS", "uid": "f1712d02a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Unified external links", "Text": "An external link type supports http, https, file and mailto protocols." }, "format": "BULLETS", "uid": "f1712df2a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Support relative paths", "Text": "Relative paths are supported for external links and pictures." }, "format": "BULLETS", "uid": "f1712ed8a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Regular expression field type", "Text": "A regular expression field type can match patterns (phone numbers, email addresses, etc.)" }, "format": "BULLETS", "uid": "f1712fbea25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Node IDs automatically generated", "Text": "Unique node IDs are automatically generated and updated." }, "format": "BULLETS", "uid": "f1713194a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "New windows installer", "Text": "A new windows installer allows a single-user, non-administrator install." }, "format": "BULLETS", "uid": "f1713284a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Portable installs", "Text": "Includes better support for portable installs." }, "format": "BULLETS", "uid": "f171336aa25a11e7b7c67054d2175f18" }, { "children": [ "f171354aa25a11e7b7c67054d2175f18", "f1713644a25a11e7b7c67054d2175f18", "f171372aa25a11e7b7c67054d2175f18" ], "data": { "Name": "Contacts" }, "format": "HEADINGS", "uid": "f1713464a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Mailing list", "Text": "There is a low-volume 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).
\n
\nTo subscribe, go to lists.sourceforge.net/lists/listinfo/treeline-users
\n" }, "format": "HEAD_PARA", "uid": "f171354aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Email", "Text": "If you do not wish to subscribe to the mailing list, I can be contacted by email at: doug101 AT bellz DOT org " }, "format": "HEAD_PARA", "uid": "f1713644a25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Updates", "Text": "I welcome any feedback, including reports of any bugs you find. Also, you can periodically check back to treeline.bellz.org for any updates." }, "format": "HEAD_PARA", "uid": "f171372aa25a11e7b7c67054d2175f18" }, { "children": [], "data": { "Name": "Network drive issues", "Text": "Work around bugs when using files located on some types of Windows network drives." }, "format": "BULLETS", "uid": "f2a0c0a1dc8d11ea8921ac675dac20af" }, { "children": [], "data": { "Name": "Error handling", "Text": "Error handling has been improved. For most errors, a dialog box is shown with debugging information that can be copied into an email." }, "format": "BULLETS", "uid": "f5983b38bb0411e795e13417ebd53aeb" }, { "children": [ "42fb4166305a11e99fd7a44cc8e97404", "014e1392305b11e9ab99a44cc8e97404", "46f3d0a4305e11e9892fa44cc8e97404", "f7bbd90a305111e98209a44cc8e97404", "221ca8f0305911e99f3fa44cc8e97404", "2c9ba0c832ed11e9bf7f7054d2175f18", "2d4430dc305711e9a33aa44cc8e97404", "1b73aaf4305811e98bb7a44cc8e97404" ], "data": { "Name": "New Features" }, "format": "BULLET_HEADING", "uid": "f7bbd909305111e9be43a44cc8e97404" }, { "children": [], "data": { "Name": "Regenerate references command", "Text": "A new Regenerate References command was added to the Data menu. It forces updates to all conditional types and math fields. It should only be necessary for comparisons to current dates/times or for externally modified data." }, "format": "BULLETS", "uid": "f7bbd90a305111e98209a44cc8e97404" }, { "children": [], "data": { "Name": "XML export", "Text": "Fixed problems with generic XML export from multiple root nodes." }, "format": "BULLETS", "uid": "f817e5b6334a11e89349d66a6ab671cb" }, { "children": [], "data": { "Name": "Spanish translation", "Text": "Added a Spanish GUI translation (thanks to Diego)." }, "format": "BULLETS", "uid": "f966db0cc23f11e8b80fd66a6ab671cb" }, { "children": [], "data": { "Name": "Printing empty branches", "Text": "Fix an error caused by attempting to print an empty branch." }, "format": "BULLETS", "uid": "fcc1b718e5df11e9a7f9a44cc8e97404" }, { "children": [ "6a6067d4482011e989f27054d2175f18", "b398d8ee482211e989f27054d2175f18", "ff3f7da6481e11e989f27054d2175f18" ], "data": { "Name": "Updates" }, "format": "BULLET_HEADING", "uid": "ff3f7c98481e11e989f27054d2175f18" }, { "children": [], "data": { "Name": "Translations", "Text": "Updated German and Spanish GUI translations (thanks to Maria Seliger and Diego)." }, "format": "BULLETS", "uid": "ff3f7da6481e11e989f27054d2175f18" } ], "properties": { "tlversion": "3.1.4", "topnodes": [ "f16f7d90a25a11e7b7c67054d2175f18" ] } }TreeLine/doc/LICENSE0000644000175000017500000004310313262465526013031 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/basichelp.html0000644000175000017500000001224213262465526014644 0ustar dougdoug basichelp Views
Tree View - The left-hand view shows a tree of node titles. Parent nodes can be opened and closed to display or hide their indented descendant nodes. Clicking on an already selected node allows the title to be edited. Right-click context menus are available for commonly used functions.

Breadcrumb View - The top pane shows the parent and ancestors of the selected node. It is blank if no nodes or multiple nodes are selected. Ancestors with blue text can be clicked to select those nodes.

Right-hand Views - The right pane is tabbed to show one of three different views of the data. The "Data Output" view shows the formatted text, the "Data Edit" view shows text edit boxes, and the "Title List" view shows an editable list of node titles.

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 a 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 (by holding down the shift or Ctrl keys while clicking), the right view will not display any child node information. It will instead show information about every selected node.

When no nodes are selected in the tree (by clicking on a blank area or Ctrl clicking to unselect), the right view will show information about the top-level (root) nodes.

Data Output View - The "Data Output" view shows formatted output text. It cannot be edited from this view.

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

Data Edit View - The "Data Edit" view shows a text edit box for each data field within a node. It also shows the node types and the node titles. The types of edit boxes vary based on the field type. Some are just text editors, while others (such as choice fields, date fields, links, etc.) have pull-down menus or dialogs.

Title List View - The "Title List" view shows a list of node titles that can be modified using typical text editor methods. If a new line is typed, a new node is created with that title. If a line is deleted, the corresponding node is removed from the tree.

Editing
Node Menu - The commands in the "Node" menu operate on the selected nodes in the left tree view. There are commands to add or insert nodes, rename node titles and delete nodes. There are also commands to rearrange the tree by changing indent levels or moving nodes up or down. For many of the commands, the descendants of the selected nodes are also affected.

Edit Menu - The edit menu includes undo and redo commands that can fix problems. Cut, copy and paste commands can operate either on text in the right-hand view (if selected or active) or to tree nodes.

Format Menu - The format menu has text formatting commands that are active when using edit boxes in the Data Edit view.

Shortcuts - There are several shortcuts for use in tree editing. Drag and drop will move (or copy if the Ctrl button is held) nodes. Clicking on a selected node will rename it. Pressing the delete key will remove the selected nodes. If desired, these shortcuts can be disabled in "Tools > General Options".

Files
Templates - When starting a new file, a dialog box offers a choice of templates. The default has only a single text field for each node that contains the title. The Long Text template adds a second long text field for more output text. Other templates have various fields for contacts, book lists and to-do lists.

Sample Files - Various TreeLine sample files can be opened by using the "File > Open Sample" command. These have more detail and example content than the new file templates.

Data Types
Node Types - Multiple node data types can be defined in a TreeLine file. Each can contain different data fields and have different output formats. See the template and sample files for examples. Nodes can be set to a specific type using the "Data > Set Node Type" command.

Type Config - The "Data > Configure Data Types" command is used to modify node data types, fields and output formatting. Refer to the Detailed Usage section of the full documentation for details.
TreeLine/uninstall.py0000755000175000017500000000527613262465526013656 0ustar dougdoug#!/usr/bin/env python """ **************************************************************************** uninstall.py, Linux uninstall script for TreeLine Copyright (C) 2018, 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' def usage(exitCode=2): """Display usage info and exit. Arguments: exitCode -- the code to retuen when exiting. """ 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: {0}]'.format(prefixDir)) sys.exit(exitCode) def removeAll(path): """Remove path, whether it is a file or a directory, print status""" print(' Removing {0}...'.format(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 as e: if str(e).find('Permission denied') >= 0: print('\nError - must be root to remove files') sys.exit(4) raise def main(): """Main uninstaller function. """ 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)) removeAll(os.path.join(prefixDir, 'share', progName)) removeAll(os.path.join(prefixDir, 'share', 'icons', progName)) removeAll(os.path.join(prefixDir, 'share', 'icons', 'hicolor', '48x48', 'apps', progName + '-icon.png')) removeAll(os.path.join(prefixDir, 'share', 'icons', 'hicolor', 'scalable', 'apps', progName + '-icon.svg')) removeAll(os.path.join(prefixDir, 'share', 'applications', progName + '.desktop')) removeAll(os.path.join(prefixDir, 'bin', progName)) print('Uninstall complete.') if __name__ == '__main__': main() TreeLine/source/0000755000175000017500000000000013760323621012546 5ustar dougdougTreeLine/source/optiondefaults.py0000644000175000017500000003627213671464603016201 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # optiondefaults.py, defines defaults for config options # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import options daysOfWeek = [_('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday'), _('Sunday')] # colorThemes = [_('Normal'), _('Dark')] def setGenOptionDefaults(generalOptions): """Set defaults for general config options. """ StringOptionItem = options.StringOptionItem IntOptionItem = options.IntOptionItem BoolOptionItem = options.BoolOptionItem ListOptionItem = options.ListOptionItem BoolOptionItem(generalOptions, 'AutoFileOpen', False, _('Startup Condition'), _('Automatically open last file used')) BoolOptionItem(generalOptions, 'InitShowBreadcrumb', True, _('Startup Condition'), _('Show breadcrumb ancestor view')) BoolOptionItem(generalOptions, 'InitShowChildPane', True, _('Startup Condition'), _('Show child pane in right hand views')) BoolOptionItem(generalOptions, 'InitShowDescendants', False, _('Startup Condition'), _('Show descendants in output view')) BoolOptionItem(generalOptions, 'SaveTreeStates', True, _('Startup Condition'), _('Restore tree view states of recent files')) BoolOptionItem(generalOptions, 'PurgeRecentFiles', True, _('Startup Condition'), _('Remove inaccessible recent file entries')) BoolOptionItem(generalOptions, 'SaveWindowGeom', True, _('Startup Condition'), _('Restore previous window geometry')) BoolOptionItem(generalOptions, 'OpenNewWindow', True, _('Features Available'), _('Open files in new windows')) BoolOptionItem(generalOptions, 'MinToSysTray', False, _('Features Available'), _('Minimize application to system tray')) BoolOptionItem(generalOptions, 'EditorOnHover', True, _('Features Available'), _('Activate data editors on mouse hover')) BoolOptionItem(generalOptions, 'EditorLimitHeight', True, _('Features Available'), _('Limit data editor height to window size')) BoolOptionItem(generalOptions, 'ClickRename', True, _('Features Available'), _('Click node to rename')) BoolOptionItem(generalOptions, 'RenameNewNodes', True, _('Features Available'), _('Rename new nodes when created')) BoolOptionItem(generalOptions, 'DragTree', True, _('Features Available'), _('Tree drag && drop available')) BoolOptionItem(generalOptions, 'PrettyPrint', False, _('Features Available'), _('Indent (pretty print) TreeLine JSON files')) BoolOptionItem(generalOptions, 'ShowTreeIcons', True, _('Features Available'), _('Show icons in the tree view')) BoolOptionItem(generalOptions, 'ShowMath', True, _('Features Available'), _('Show math fields in the Data Edit view')) BoolOptionItem(generalOptions, 'EditNumbering', False, _('Features Available'), _('Show numbering fields in the Data Edit view')) IntOptionItem(generalOptions, 'UndoLevels', 5, 0, 999, _('Undo Memory'), _('Number of undo levels'), 1) IntOptionItem(generalOptions, 'AutoSaveMinutes', 0, 0, 999, _('Auto Save'), _('Minutes between saves\n(set to 0 to disable)'), 1) IntOptionItem(generalOptions, 'RecentFiles', 4, 0, 99, _('Recent Files'), _('Number of recent files \nin the file menu'), 1) StringOptionItem(generalOptions, 'EditTimeFormat', '%-H:%M:%S', False, True, _('Data Editor Formats'), _('Times'), 1) StringOptionItem(generalOptions, 'EditDateFormat', '%m/%d/%y', False, True, _('Data Editor Formats'), _('Dates'), 1) ListOptionItem(generalOptions, 'WeekStart', daysOfWeek[-1], daysOfWeek, _('Data Editor Formats'), _('First day\nof week'), 1) IntOptionItem(generalOptions, 'IndentOffset', 2, 0, 99, _('Appearance'), _('Child indent offset\n(in font height units) '), 1) # ListOptionItem(generalOptions, 'ColorTheme', colorThemes[0], colorThemes, # _('Appearance'), _('Color Theme'), 1) def setMiscOptionDefaults(miscOptions): """Set defaults for miscellaneous config options. """ StringOptionItem = options.StringOptionItem StringOptionItem(miscOptions, 'PrintUnits', 'in', False, True) StringOptionItem(miscOptions, 'SpellCheckPath', '') StringOptionItem(miscOptions, 'AppFont', '', True, True) StringOptionItem(miscOptions, 'TreeFont', '', True, True) StringOptionItem(miscOptions, 'OutputFont', '', True, True) StringOptionItem(miscOptions, 'EditorFont', '', True, True) StringOptionItem(miscOptions, 'ColorTheme', 'system', False, True) StringOptionItem(miscOptions, 'WindowColor', '', True, True) StringOptionItem(miscOptions, 'WindowTextColor', '', True, True) StringOptionItem(miscOptions, 'BaseColor', '', True, True) StringOptionItem(miscOptions, 'TextColor', '', True, True) StringOptionItem(miscOptions, 'HighlightColor', '', True, True) StringOptionItem(miscOptions, 'HighlightedTextColor', '', True, True) StringOptionItem(miscOptions, 'LinkColor', '', True, True) StringOptionItem(miscOptions, 'ToolTipBaseColor', '', True, True) StringOptionItem(miscOptions, 'ToolTipTextColor', '', True, True) StringOptionItem(miscOptions, 'ButtonColor', '', True, True) StringOptionItem(miscOptions, 'ButtonTextColor', '', True, True) StringOptionItem(miscOptions, 'Text-DisabledColor', '', True, True) StringOptionItem(miscOptions, 'ButtonText-DisabledColor', '', True, True) def setHistOptionDefaults(historyOptions): """Set defaults for history config options. """ IntOptionItem = options.IntOptionItem DataListOptionItem = options.DataListOptionItem IntOptionItem(historyOptions, 'WindowXSize', 640, 10, 10000) IntOptionItem(historyOptions, 'WindowYSize', 640, 10, 10000) IntOptionItem(historyOptions, 'WindowXPos', -1000, -1000, 10000) IntOptionItem(historyOptions, 'WindowYPos', -1000, -1000, 10000) IntOptionItem(historyOptions, 'WindowTopMargin', 0, 0, 1000) IntOptionItem(historyOptions, 'WindowOtherMargin', 0, 0, 1000) IntOptionItem(historyOptions, 'CrumbSplitPercent', 10, 1, 99) IntOptionItem(historyOptions, 'TreeSplitPercent', 40, 1, 99) IntOptionItem(historyOptions, 'OutputSplitPercent', 20, 1, 99) IntOptionItem(historyOptions, 'EditorSplitPercent', 25, 1, 99) IntOptionItem(historyOptions, 'TitleSplitPercent', 10, 1, 99) IntOptionItem(historyOptions, 'ActiveRightView', 0, 0, 2) IntOptionItem(historyOptions, 'PrintPrevXSize', 0, 0, 10000) IntOptionItem(historyOptions, 'PrintPrevYSize', 0, 0, 10000) IntOptionItem(historyOptions, 'PrintPrevXPos', -1000, -1000, 10000) IntOptionItem(historyOptions, 'PrintPrevYPos', -1000, -1000, 10000) DataListOptionItem(historyOptions, 'RecentFiles', []) def setToolbarOptionDefaults(toolbarOptions): """Set defaults for toolbar geometry and buttons. """ StringOptionItem = options.StringOptionItem DataListOptionItem = options.DataListOptionItem IntOptionItem = options.IntOptionItem IntOptionItem(toolbarOptions, 'ToolbarQuantity', 2, 0, 20) IntOptionItem(toolbarOptions, 'ToolbarSize', 16, 1, 128) StringOptionItem(toolbarOptions, 'ToolbarPosition', '') DataListOptionItem(toolbarOptions, 'ToolbarCommands', ['FileNew,FileOpen,FileSave,,FilePrintPreview,' 'FilePrint,,EditUndo,EditRedo,,EditCut,EditCopy,' 'EditPaste,,DataConfigType,ToolsFindText', 'NodeInsertAfter,NodeAddChild,,NodeDelete,NodeIndent,' 'NodeUnindent,,NodeMoveUp,NodeMoveDown,,' 'ViewExpandBranch,ViewCollapseBranch,,' 'ViewPrevSelect,ViewNextSelect,,ViewShowDescend']) def setKeyboardOptionDefaults(keyboardOptions): """Set defaults for keyboard shortcuts. """ KeyOptionItem = options.KeyOptionItem KeyOptionItem(keyboardOptions, 'FileNew', 'Ctrl+N', 'File Menu') KeyOptionItem(keyboardOptions, 'FileOpen', 'Ctrl+O', 'File Menu') KeyOptionItem(keyboardOptions, 'FileOpenSample', '', 'File Menu') KeyOptionItem(keyboardOptions, 'FileImport', '', 'File Menu') KeyOptionItem(keyboardOptions, 'FileSave', 'Ctrl+S', 'File Menu') KeyOptionItem(keyboardOptions, 'FileSaveAs', '', 'File Menu') KeyOptionItem(keyboardOptions, 'FileExport', '', 'File Menu') KeyOptionItem(keyboardOptions, 'FileProperties', '', 'File Menu') KeyOptionItem(keyboardOptions, 'FilePrintSetup', '', 'File Menu') KeyOptionItem(keyboardOptions, 'FilePrintPreview', '', 'File Menu') KeyOptionItem(keyboardOptions, 'FilePrint', 'Ctrl+P', 'File Menu') KeyOptionItem(keyboardOptions, 'FilePrintPdf', '', 'File Menu') KeyOptionItem(keyboardOptions, 'FileQuit', 'Ctrl+Q', 'File Menu') KeyOptionItem(keyboardOptions, 'EditUndo', 'Ctrl+Z', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditRedo', 'Ctrl+Y', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditCut', 'Ctrl+X', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditCopy', 'Ctrl+C', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditPaste', 'Ctrl+V', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditPastePlain', '', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditPasteChild', '', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditPasteBefore', '', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditPasteAfter', '', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditPasteCloneChild', '', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditPasteCloneBefore', '', 'Edit Menu') KeyOptionItem(keyboardOptions, 'EditPasteCloneAfter', '', 'Edit Menu') KeyOptionItem(keyboardOptions, 'NodeRename', 'Ctrl+R', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeAddChild', 'Ctrl+A', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeInsertBefore', 'Ctrl+B', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeInsertAfter', 'Ctrl+I', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeDelete', 'Del', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeIndent', 'Ctrl+Shift+Right', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeUnindent', 'Ctrl+Shift+Left', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeMoveUp', 'Ctrl+Shift+Up', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeMoveDown', 'Ctrl+Shift+Down', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeMoveFirst', '', 'Node Menu') KeyOptionItem(keyboardOptions, 'NodeMoveLast', '', 'Node Menu') KeyOptionItem(keyboardOptions, 'DataNodeType', 'Ctrl+T', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataConfigType', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataCopyType', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataVisualConfig', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataSortNodes', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataNumbering', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataRegenRefs', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataCloneMatches', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataDetachClones', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataFlatCategory', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataAddCategory', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'DataSwapCategory', '', 'Data Menu') KeyOptionItem(keyboardOptions, 'ToolsFindText', 'Ctrl+F', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsFindCondition', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsFindReplace', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsFilterText', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsFilterCondition', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsSpellCheck', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsGenOptions', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsShortcuts', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsToolbars', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsFonts', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'ToolsColors', '', 'Tools Menu') KeyOptionItem(keyboardOptions, 'FormatBoldFont', '', 'Format Menu') KeyOptionItem(keyboardOptions, 'FormatItalicFont', '', 'Format Menu') KeyOptionItem(keyboardOptions, 'FormatUnderlineFont', '', 'Format Menu') KeyOptionItem(keyboardOptions, 'FormatFontSize', '', 'Format Menu') KeyOptionItem(keyboardOptions, 'FormatFontColor', '', 'Format Menu') KeyOptionItem(keyboardOptions, 'FormatExtLink', '', 'Format Menu') KeyOptionItem(keyboardOptions, 'FormatIntLink', '', 'Format Menu') KeyOptionItem(keyboardOptions, 'FormatInsertDate', '', 'Format Menu') KeyOptionItem(keyboardOptions, 'FormatSelectAll', 'Ctrl+L', 'Format Menu') KeyOptionItem(keyboardOptions, 'FormatClearFormat', '', 'Format Menu') KeyOptionItem(keyboardOptions, 'ViewExpandBranch', 'Ctrl+Right', 'View Menu') KeyOptionItem(keyboardOptions, 'ViewCollapseBranch', 'Ctrl+Left', 'View Menu') KeyOptionItem(keyboardOptions, 'ViewPrevSelect', 'Ctrl+Shift+P', 'View Menu') KeyOptionItem(keyboardOptions, 'ViewNextSelect', 'Ctrl+Shift+N', 'View Menu') KeyOptionItem(keyboardOptions, 'ViewDataOutput', 'Ctrl+Shift+O', 'View Menu') KeyOptionItem(keyboardOptions, 'ViewDataEditor', 'Ctrl+Shift+E', 'View Menu') KeyOptionItem(keyboardOptions, 'ViewTitleList', 'Ctrl+Shift+T', 'View Menu') KeyOptionItem(keyboardOptions, 'ViewBreadcrumb', '', 'View Menu') KeyOptionItem(keyboardOptions, 'ViewShowChildPane', 'Ctrl+Shift+C', 'View Menu') KeyOptionItem(keyboardOptions, 'ViewShowDescend', 'Ctrl+Shift+D', 'View Menu') KeyOptionItem(keyboardOptions, 'WinNewWindow', '', 'Window Menu') KeyOptionItem(keyboardOptions, 'WinCloseWindow', '', 'Window Menu') KeyOptionItem(keyboardOptions, 'HelpBasic', '', 'Help Menu') KeyOptionItem(keyboardOptions, 'HelpFull', '', 'Help Menu') KeyOptionItem(keyboardOptions, 'HelpAbout', '', 'Help Menu') KeyOptionItem(keyboardOptions, 'IncremSearchStart', 'Ctrl+/', 'No Menu') KeyOptionItem(keyboardOptions, 'IncremSearchNext', 'F3', 'No Menu') KeyOptionItem(keyboardOptions, 'IncremSearchPrev', 'Shift+F3', 'No Menu') TreeLine/source/treestructure.py0000644000175000017500000003147513363127527016060 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treestructure.py, provides a class to store the tree's data # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import operator import copy import json import uuid import treenode import treeformats import undo try: from __main__ import __version__ except ImportError: __version__ = '' defaultRootTitle = _('Main') class TreeStructure(treenode.TreeNode): """Class to store all tree data. Inherits TreeNode to get childList (holds top nodes) and other methods. """ def __init__(self, fileData=None, topNodes=None, addDefaults=False, addSpots=True): """Create and store a tree structure from file data. If no file data is given, create an empty or a default new structure. Arguments: fileData -- a dict in JSON file format of a structure topNodes -- existing top-level nodes to add to a structure addDefaults -- if True, adds default new structure addSpots -- if True, adds parent spot references """ super().__init__(None) # init TreeNode, with no formatRef self.nodeDict = {} self.undoList = None self.redoList = None self.configDialogFormats = None self.mathZeroBlanks = True self.childRefErrorNodes = [] if fileData: self.treeFormats = treeformats.TreeFormats(fileData['formats']) self.treeFormats.loadGlobalSavedConditions(fileData['properties']) for nodeInfo in fileData['nodes']: formatRef = self.treeFormats[nodeInfo['format']] node = treenode.TreeNode(formatRef, nodeInfo) self.nodeDict[node.uId] = node for node in self.nodeDict.values(): node.assignRefs(self.nodeDict) if node.tmpChildRefs: self.childRefErrorNodes.append(node) node.tmpChildRefs = [] for uId in fileData['properties']['topnodes']: node = self.nodeDict[uId] self.childList.append(node) if 'zeroblanks' in fileData['properties']: self.mathZeroBlanks = fileData['properties']['zeroblanks'] if addSpots: self.generateSpots(None) elif topNodes: self.childList = topNodes self.treeFormats = treeformats.TreeFormats() for topNode in topNodes: for node in topNode.descendantGen(): self.nodeDict[node.uId] = node self.treeFormats.addTypeIfMissing(node.formatRef) if addSpots: self.generateSpots(None) elif addDefaults: self.treeFormats = treeformats.TreeFormats(setDefault=True) node = treenode.TreeNode(self.treeFormats[treeformats. defaultTypeName]) node.setTitle(defaultRootTitle) self.nodeDict[node.uId] = node self.childList.append(node) if addSpots: self.generateSpots(None) else: self.treeFormats = treeformats.TreeFormats() self.fileInfoNode = treenode.TreeNode(self.treeFormats.fileInfoFormat) def fileData(self): """Return a fileData dict in JSON file format. """ formats = self.treeFormats.storeFormats() nodeList = sorted([node.fileData() for node in self.nodeDict.values()], key=operator.itemgetter('uid')) topNodeIds = [node.uId for node in self.childList] properties = {'tlversion': __version__, 'topnodes': topNodeIds} self.treeFormats.storeGlobalSavedConditions(properties) if not self.mathZeroBlanks: properties['zeroblanks'] = False fileData = {'formats': formats, 'nodes': nodeList, 'properties': properties} return fileData def purgeOldFieldData(self): """Remove data from obsolete fields from all nodes. """ fieldSets = self.treeFormats.fieldNameDict() for node in self.nodeDict.values(): oldKeys = set(node.data.keys()) - fieldSets[node.formatRef.name] for key in oldKeys: del node.data[key] def addNodeDictRef(self, node): """Add the given node to the node dictionary. Arguments: node -- the node to add """ self.nodeDict[node.uId] = node def removeNodeDictRef(self, node): """Remove the given node from the node dictionary. Arguments: node -- the node to remove """ try: del self.nodeDict[node.uId] except KeyError: pass def rebuildNodeDict(self): """Remove and re-create the entire node dictionary. """ self.nodeDict = {} for node in self.descendantGen(): self.nodeDict[node.uId] = node def replaceAllSpots(self, removeUnusedNodes=True): """Remove and regenerate all spot refs for the tree. Arguments: removeUnusedNodes -- if True, delete refs to nodes without spots """ self.spotRefs = set() for node in self.nodeDict.values(): node.spotRefs = set() self.generateSpots(None) if removeUnusedNodes: self.nodeDict = {uId:node for (uId, node) in self.nodeDict.items() if node.spotRefs} def deleteNodeSpot(self, spot): """Remove the given spot, removing the entire node if no spots remain. Arguments: spot -- the spot to remove """ spot.parentSpot.nodeRef.childList.remove(spot.nodeRef) for node in spot.nodeRef.descendantGen(): if len(node.spotRefs) <= 1: self.removeNodeDictRef(node) node.spotRefs = set() else: node.removeInvalidSpotRefs(False) def structSpot(self): """Return the top spot (not tied to a node). """ (topSpot, ) = self.spotRefs return topSpot def rootSpots(self): """Return a list of spots from root nodes. """ (topSpot, ) = self.spotRefs return topSpot.childSpots() def spotById(self, spotId): """Return a spot based on a spot ID string. Raises KeyError on invalid node ID, an IndexError on invalid spot num. Arguments: spotId -- a spot ID string, in the form "nodeID:spotInstance" """ nodeId, spotNum = spotId.split(':', 1) return self.nodeDict[nodeId].spotByNumber(int(spotNum)) def descendantGen(self): """Return a generator to step through all nodes in tree order. Override from TreeNode to exclude self. """ for child in self.childList: for node in child.descendantGen(): yield node def getConfigDialogFormats(self, forceReset=False): """Return duplicate formats for use in the config dialog. Arguments: forceReset -- if True, sets duplicate formats back to original """ if not self.configDialogFormats or forceReset: self.configDialogFormats = copy.deepcopy(self.treeFormats) return self.configDialogFormats def applyConfigDialogFormats(self, addUndo=True): """Replace the formats with the duplicates and signal for view update. Also updates all nodes for changed type and field names. """ if addUndo: undo.FormatUndo(self.undoList, self.treeFormats, self.configDialogFormats) self.treeFormats.copySettings(self.configDialogFormats) self.treeFormats.updateDerivedRefs() self.treeFormats.updateMathFieldRefs() if self.configDialogFormats.fieldRenameDict: for node in self.nodeDict.values(): fieldRenameDict = (self.configDialogFormats.fieldRenameDict. get(node.formatRef.name, {})) tmpDataDict = {} for oldName, newName in fieldRenameDict.items(): if oldName in node.data: tmpDataDict[newName] = node.data[oldName] del node.data[oldName] node.data.update(tmpDataDict) self.configDialogFormats.fieldRenameDict = {} if self.treeFormats.emptiedMathDict: for node in self.nodeDict.values(): for fieldName in self.treeFormats.emptiedMathDict.get(node. formatRef. name, set()): node.data.pop(fieldName, None) self.formats.emptiedMathDict = {} def usesType(self, typeName): """Return true if any nodes use the give node format type. Arguments: typeName -- the format name to search for """ for node in self.nodeDict.values(): if node.formatRef.name == typeName: return True return False def replaceDuplicateIds(self, duplicateDict): """Generate new unique IDs for any nodes found in newNodeDict. Arguments: newNodeDict -- a dict to search for duplicates """ for node in list(self.nodeDict.values()): if node.uId in duplicateDict: del self.nodeDict[node.uId] node.uId = uuid.uuid1().hex self.nodeDict[node.uId] = node def addNodesFromStruct(self, treeStruct, parent, position=-1): """Add nodes from the given structure under the given parent. Arguments: treeStruct -- the structure to insert parent -- the parent of the new nodes position -- the location to insert (-1 is appended) """ for nodeFormat in treeStruct.treeFormats.values(): self.treeFormats.addTypeIfMissing(nodeFormat) for node in treeStruct.nodeDict.values(): self.nodeDict[node.uId] = node node.formatRef = self.treeFormats[node.formatRef.name] for node in treeStruct.childList: if position >= 0: parent.childList.insert(position, node) position += 1 else: parent.childList.append(node) node.addSpotRef(parent) def debugCheck(self): """Run debugging checks on structure nodeDict, nodes and spots. Reports results to std output. Not to be run in production releases. """ print('\nChecking nodes in nodeDict:') nodeIds = set() errorCount = 0 for node in self.descendantGen(): nodeIds.add(node.uId) if node.uId not in self.nodeDict: print(' Node not in nodeDict, ID: {}, Title: {}'. format(node.uId, node.title())) errorCount += 1 for uId in set(self.nodeDict.keys()) - nodeIds: node = self.nodeDict[uId] print(' Node not in structure, ID: {}, Title: {}'. format(node.uId, node.title())) errorCount += 1 print(' {} errors found in nodeDict'.format(errorCount)) print('\nChecking spots:') errorCount = 0 for topNode in self.childList: for node in topNode.descendantGen(): for spot in node.spotRefs: if node not in spot.parentSpot.nodeRef.childList: print(' Invalid spot in node, ID: {}, Title: {}'. format(node.uId, node.title())) errorCount += 1 for child in node.childList: if len(child.spotRefs) < len(node.spotRefs): print(' Missing spot in node, ID: {}, Title: {}'. format(child.uId, child.title())) errorCount += 1 print(' {} errors found in spots'.format(errorCount)) #### Utility Functions #### def structFromMimeData(mimeData): """Return a tree structure based on mime data. Arguments: mimeData -- data to be used """ try: data = json.loads(str(mimeData.data('application/json'), 'utf-8')) return TreeStructure(data, addSpots=False) except (ValueError, KeyError, TypeError): return None TreeLine/source/recentfiles.py0000644000175000017500000002153613702672477015446 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # recentfiles.py, classes to save recent file lists, states and actions # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import pathlib import os.path import time from PyQt5.QtWidgets import QAction import globalref _maxActionPathLength = 30 _maxOpenNodesStored = 100 class RecentFileItem: """Class containing path, state and action info for a single recent file. """ def __init__(self, pathObj=None, dataDict=None): """Initialize with either a pathObject or a stored data dict. Arguments: pathObj -- a path object for the file dataDict -- dict of staore data """ if not dataDict: dataDict = {} self.pathObj = pathObj path = dataDict.get('path', '') if not self.pathObj and path: self.pathObj = pathlib.Path(path) self.stateTime = dataDict.get('time', 0) self.scrollPos = dataDict.get('scroll', '') self.selectSpots = dataDict.get('select', []) self.openSpots = dataDict.get('open', []) def dataDict(self): """Return the data dict for storing this recent file. """ return {'path': str(self.pathObj), 'time': self.stateTime, 'scroll': self.scrollPos, 'select': self.selectSpots, 'open': self.openSpots} def pathIsValid(self): """Return True if the current path points to an actual file. """ try: return self.pathObj.is_file() except OSError: return False def itemAction(self, posNum): """Return a menu action for this recent file. Arguments: posNum -- the position number in the menu """ abbrevPath = str(self.pathObj) if len(abbrevPath) > _maxActionPathLength: truncLength = _maxActionPathLength - 3 pos = abbrevPath.find(os.sep, len(abbrevPath) - truncLength) if pos < 0: pos = len(abbrevPath) - truncLength abbrevPath = '...' + abbrevPath[pos:] text = '&{0:d} {1}'.format(posNum, abbrevPath) action = QAction(text, globalref.mainControl, statusTip=str(self.pathObj)) action.triggered.connect(self.openFile) return action def openFile(self): """Open this path using the main control method. """ globalref.mainControl.openFile(self.pathObj, checkModified=True) def recordTreeState(self, localControl): """Save the tree state of this item. Arguments: localControl -- the control to store """ self.stateTime = int(time.time()) treeView = localControl.activeWindow.treeView topSpot = treeView.spotAtTop() self.scrollPos = topSpot.spotId() if topSpot else '' self.selectSpots = [spot.spotId() for spot in treeView.selectionModel().selectedSpots()] self.openSpots = [spot.spotId() for spot in localControl.structure. structSpot().expandedSpotDescendantGen(treeView)] self.openSpots = self.openSpots[:_maxOpenNodesStored] def restoreTreeState(self, localControl): """Restore the tree state of this item. Return True if the state was restored. Arguments: localControl -- the control to set state """ fileModTime = self.pathObj.stat().st_mtime if self.stateTime == 0 or fileModTime > self.stateTime: return False # file modified externally treeView = localControl.activeWindow.treeView try: for spotId in self.openSpots: treeView.expandSpot(localControl.structure.spotById(spotId)) if self.scrollPos: treeView.scrollToSpot(localControl.structure. spotById(self.scrollPos)) if self.selectSpots: treeView.selectionModel().selectSpots([localControl.structure. spotById(spotId) for spotId in self.selectSpots]) return True except (KeyError, IndexError): # for old TreeLine import (spotIds don't match) return False def __eq__(self, other): """Test for equality between RecentFileItems and paths. Arguments: other -- either a RecentFileItem or a path string """ try: otherPath = other.pathObj except AttributeError: otherPath = other # use abspath() - pathlib's resolve() can be buggy with network drives return (os.path.abspath(str(self.pathObj)) == os.path.abspath(str(otherPath))) def __ne__(self, other): """Test for inequality between RecentFileItems and paths. Arguments: other -- either a RecentFileItem or a path string """ try: otherPath = other.pathObj except AttributeError: otherPath = other # use abspath() - pathlib's resolve() can be buggy with network drives return (os.path.abspath(str(self.pathObj)) != os.path.abspath(str(otherPath))) class RecentFileList(list): """A list of recent file items. """ def __init__(self): """Load the initial list from the options file. """ super().__init__() self.updateOptions() for data in globalref.histOptions['RecentFiles']: item = RecentFileItem(dataDict=data) if not self.purge or item.pathIsValid(): self.append(item) def updateOptions(self): """Get number of entries and check exists from general options. """ self.numEntries = globalref.genOptions['RecentFiles'] self.purge = globalref.genOptions['PurgeRecentFiles'] def writeItems(self): """Write the recent items to the options file. """ data = [item.dataDict() for item in self[:self.numEntries]] globalref.histOptions.changeValue('RecentFiles', data) def addItem(self, pathObj): """Add the given path at the start of the list. If the path is in the list, move it to the start, otherwise create a new item. Arguments: pathObj -- the new path object to search and/or create """ item = RecentFileItem(pathObj) try: item = self.pop(self.index(item)) except ValueError: pass self.insert(0, item) def removeItem(self, pathObj): """Remove the given path name if found. Arguments: pathObj -- the path to be removed """ try: self.remove(RecentFileItem(pathObj)) except ValueError: pass def getActions(self): """Return a list of actions for ech recent item. """ return [item.itemAction(i) for i, item in enumerate(self[:self.numEntries], 1)] def firstDir(self): """Return a path object of the first valid directory from recent items. """ for item in self: pathObj = item.pathObj.parent try: if pathObj.is_dir(): return pathObj except OSError: pass return None def firstPath(self): """Return the first full path from the recent items if valid. """ if self and self[0].pathIsValid(): return self[0].pathObj return None def saveTreeState(self, localControl): """Save the tree state of the item matching the localControl. Arguments: localControl -- the control to store """ try: item = self[self.index(localControl.filePathObj)] except (ValueError, TypeError, AttributeError, OSError): return item.recordTreeState(localControl) def retrieveTreeState(self, localControl): """Restore the saved tree state of the item matching the localControl. Return True if the state was restored. Arguments: localControl -- the control to restore state """ try: item = self[self.index(localControl.filePathObj)] except (ValueError, AttributeError): return False return item.restoreTreeState(localControl) TreeLine/source/printdialogs.py0000644000175000017500000016046013363127527015634 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # printdialogs.py, provides print preview and print settings dialogs # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import collections from PyQt5.QtCore import (QMarginsF, QPoint, QRect, QSize, QSizeF, Qt, pyqtSignal) from PyQt5.QtGui import (QFontDatabase, QFontInfo, QFontMetrics, QIntValidator, QPageLayout, QPageSize) from PyQt5.QtWidgets import (QAbstractItemView, QAction, QButtonGroup, QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QMenu, QMessageBox, QPushButton, QRadioButton, QSpinBox, QTabWidget, QToolBar, QVBoxLayout, QWidget) from PyQt5.QtPrintSupport import (QPrintPreviewWidget, QPrinter, QPrinterInfo) import printdata import configdialog import treeformats import undo import globalref class PrintPreviewDialog(QDialog): """Dialog for print previews. Similar to QPrintPreviewDialog but calls a custom page setup dialog. """ def __init__(self, printData, parent=None): """Create the print preview dialog. Arguments: printData -- the PrintData object parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Print Preview')) self.printData = printData topLayout = QVBoxLayout(self) self.setLayout(topLayout) toolBar = QToolBar(self) topLayout.addWidget(toolBar) self.previewWidget = QPrintPreviewWidget(printData.printer, self) topLayout.addWidget(self.previewWidget) self.previewWidget.previewChanged.connect(self.updateControls) self.zoomWidthAct = QAction(_('Fit Width'), self, checkable=True) icon = globalref.toolIcons.getIcon('printpreviewzoomwidth') if icon: self.zoomWidthAct.setIcon(icon) self.zoomWidthAct.triggered.connect(self.zoomWidth) toolBar.addAction(self.zoomWidthAct) self.zoomAllAct = QAction(_('Fit Page'), self, checkable=True) icon = globalref.toolIcons.getIcon('printpreviewzoomall') if icon: self.zoomAllAct.setIcon(icon) self.zoomAllAct.triggered.connect(self.zoomAll) toolBar.addAction(self.zoomAllAct) toolBar.addSeparator() self.zoomCombo = QComboBox(self) self.zoomCombo.setEditable(True) self.zoomCombo.setInsertPolicy(QComboBox.NoInsert) self.zoomCombo.addItems([' 12%', ' 25%', ' 50%', ' 75%', ' 100%', ' 125%', ' 150%', ' 200%', ' 400%', ' 800%']) self.zoomCombo.currentIndexChanged[str].connect(self.zoomToValue) self.zoomCombo.lineEdit().returnPressed.connect(self.zoomToValue) toolBar.addWidget(self.zoomCombo) zoomInAct = QAction(_('Zoom In'), self) icon = globalref.toolIcons.getIcon('printpreviewzoomin') if icon: zoomInAct.setIcon(icon) zoomInAct.triggered.connect(self.zoomIn) toolBar.addAction(zoomInAct) zoomOutAct = QAction(_('Zoom Out'), self) icon = globalref.toolIcons.getIcon('printpreviewzoomout') if icon: zoomOutAct.setIcon(icon) zoomOutAct.triggered.connect(self.zoomOut) toolBar.addAction(zoomOutAct) toolBar.addSeparator() self.previousAct = QAction(_('Previous Page'), self) icon = globalref.toolIcons.getIcon('printpreviewprevious') if icon: self.previousAct.setIcon(icon) self.previousAct.triggered.connect(self.previousPage) toolBar.addAction(self.previousAct) self.pageNumEdit = QLineEdit(self) self.pageNumEdit.setAlignment(Qt.AlignRight | Qt.AlignVCenter) width = QFontMetrics(self.pageNumEdit.font()).width('0000') self.pageNumEdit.setMaximumWidth(width) self.pageNumEdit.returnPressed.connect(self.setPageNum) toolBar.addWidget(self.pageNumEdit) self.maxPageLabel = QLabel(' / 000 ', self) toolBar.addWidget(self.maxPageLabel) self.nextAct = QAction(_('Next Page'), self) icon = globalref.toolIcons.getIcon('printpreviewnext') if icon: self.nextAct.setIcon(icon) self.nextAct.triggered.connect(self.nextPage) toolBar.addAction(self.nextAct) toolBar.addSeparator() self.onePageAct = QAction(_('Single Page'), self, checkable=True) icon = globalref.toolIcons.getIcon('printpreviewsingle') if icon: self.onePageAct.setIcon(icon) self.onePageAct.triggered.connect(self.previewWidget. setSinglePageViewMode) toolBar.addAction(self.onePageAct) self.twoPageAct = QAction(_('Facing Pages'), self, checkable=True) icon = globalref.toolIcons.getIcon('printpreviewdouble') if icon: self.twoPageAct.setIcon(icon) self.twoPageAct.triggered.connect(self.previewWidget. setFacingPagesViewMode) toolBar.addAction(self.twoPageAct) toolBar.addSeparator() pageSetupAct = QAction(_('Print Setup'), self) icon = globalref.toolIcons.getIcon('fileprintsetup') if icon: pageSetupAct.setIcon(icon) pageSetupAct.triggered.connect(self.printSetup) toolBar.addAction(pageSetupAct) filePrintAct = QAction(_('Print'), self) icon = globalref.toolIcons.getIcon('fileprint') if icon: filePrintAct.setIcon(icon) filePrintAct.triggered.connect(self.filePrint) toolBar.addAction(filePrintAct) def updateControls(self): """Update control availability and status based on a change signal. """ self.zoomWidthAct.setChecked(self.previewWidget.zoomMode() == QPrintPreviewWidget.FitToWidth) self.zoomAllAct.setChecked(self.previewWidget.zoomMode() == QPrintPreviewWidget.FitInView) zoom = self.previewWidget.zoomFactor() * 100 self.zoomCombo.setEditText('{0:4.0f}%'.format(zoom)) self.previousAct.setEnabled(self.previewWidget.currentPage() > 1) self.nextAct.setEnabled(self.previewWidget.currentPage() < self.previewWidget.pageCount()) self.pageNumEdit.setText(str(self.previewWidget.currentPage())) self.maxPageLabel.setText(' / {0} '.format(self.previewWidget. pageCount())) self.onePageAct.setChecked(self.previewWidget.viewMode() == QPrintPreviewWidget.SinglePageView) self.twoPageAct.setChecked(self.previewWidget.viewMode() == QPrintPreviewWidget.FacingPagesView) def zoomWidth(self, checked=True): """Set the fit to width zoom mode if checked. Arguments: checked -- set this mode if True """ if checked: self.previewWidget.setZoomMode(QPrintPreviewWidget. FitToWidth) else: self.previewWidget.setZoomMode(QPrintPreviewWidget. CustomZoom) self.updateControls() def zoomAll(self, checked=True): """Set the fit in view zoom mode if checked. Arguments: checked -- set this mode if True """ if checked: self.previewWidget.setZoomMode(QPrintPreviewWidget.FitInView) else: self.previewWidget.setZoomMode(QPrintPreviewWidget. CustomZoom) self.updateControls() def zoomToValue(self, factorStr=''): """Zoom to the given combo box string value. Arguments: factorStr -- the zoom factor as a string, often with a % suffix """ if not factorStr: factorStr = self.zoomCombo.lineEdit().text() try: factor = float(factorStr.strip(' %')) / 100 self.previewWidget.setZoomFactor(factor) except ValueError: pass self.updateControls() def zoomIn(self): """Increase the zoom level by an increment. """ self.previewWidget.zoomIn() self.updateControls() def zoomOut(self): """Decrease the zoom level by an increment. """ self.previewWidget.zoomOut() self.updateControls() def previousPage(self): """Go to the previous page of the preview. """ self.previewWidget.setCurrentPage(self.previewWidget.currentPage() - 1) self.updateControls() def nextPage(self): """Go to the next page of the preview. """ self.previewWidget.setCurrentPage(self.previewWidget.currentPage() + 1) self.updateControls() def setPageNum(self): """Go to a page number from the line editor based on a signal. """ try: self.previewWidget.setCurrentPage(int(self.pageNumEdit.text())) except ValueError: pass self.updateControls() def printSetup(self): """Show a dialog to set margins, page size and other printing options. """ setupDialog = PrintSetupDialog(self.printData, False, self) if setupDialog.exec_() == QDialog.Accepted: self.printData.setupData() self.previewWidget.updatePreview() def filePrint(self): """Show dialog and print tree output based on current options. """ self.close() if self.printData.printer.outputFormat() == QPrinter.NativeFormat: self.printData.filePrint() else: self.printData.filePrintPdf() def sizeHint(self): """Return a larger default height. """ size = super().sizeHint() size.setHeight(600) return size def restoreDialogGeom(self): """Restore dialog window geometry from history options. """ rect = QRect(globalref.histOptions['PrintPrevXPos'], globalref.histOptions['PrintPrevYPos'], globalref.histOptions['PrintPrevXSize'], globalref.histOptions['PrintPrevYSize']) if rect.height() and rect.width(): self.setGeometry(rect) def saveDialogGeom(self): """Savedialog window geometry to history options. """ globalref.histOptions.changeValue('PrintPrevXSize', self.width()) globalref.histOptions.changeValue('PrintPrevYSize', self.height()) globalref.histOptions.changeValue('PrintPrevXPos', self.geometry().x()) globalref.histOptions.changeValue('PrintPrevYPos', self.geometry().y()) def closeEvent(self, event): """Save dialog geometry at close. Arguments: event -- the close event """ if globalref.genOptions['SaveWindowGeom']: self.saveDialogGeom() class PrintSetupDialog(QDialog): """Base dialog for setting the print configuration. Pushes most options to the PrintData class. """ def __init__(self, printData, showExtraButtons=True, parent=None): """Create the printing setup dialog. Arguments: printData -- a reference to the PrintData class showExtraButtons -- add print preview and print shortcut buttons parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Printing Setup')) self.printData = printData topLayout = QVBoxLayout(self) self.setLayout(topLayout) tabs = QTabWidget() topLayout.addWidget(tabs) generalPage = GeneralPage(self.printData) tabs.addTab(generalPage, _('&General Options')) pageSetupPage = PageSetupPage(self.printData, generalPage.currentPrinterName) tabs.addTab(pageSetupPage, _('Page &Setup')) fontPage = FontPage(self.printData) tabs.addTab(fontPage, _('&Font Selection')) headerPage = HeaderPage(self.printData) tabs.addTab(headerPage, _('&Header/Footer')) generalPage.printerChanged.connect(pageSetupPage.changePrinter) self.tabPages = [generalPage, pageSetupPage, fontPage, headerPage] ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch() if showExtraButtons: previewButton = QPushButton(_('Print Pre&view...')) ctrlLayout.addWidget(previewButton) previewButton.clicked.connect(self.preview) printButton = QPushButton(_('&Print...')) ctrlLayout.addWidget(printButton) printButton.clicked.connect(self.quickPrint) okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) def quickPrint(self): """Accept this dialog and go to print dialog. """ self.accept() if self.printData.printer.outputFormat() == QPrinter.NativeFormat: self.printData.filePrint() else: self.printData.filePrintPdf() def preview(self): """Accept this dialog and go to print preview dialog. """ self.accept() self.printData.printPreview() def accept(self): """Store results before closing dialog. """ if not self.tabPages[1].checkValid(): QMessageBox.warning(self, 'TreeLine', _('Error: Page size or margins are invalid')) return changed = False control = self.printData.localControl undoObj = undo.StateSettingUndo(control.structure.undoList, self.printData.fileData, self.printData.readData) for page in self.tabPages: if page.saveChanges(): changed = True if changed: self.printData.adjustSpacing() control.setModified() else: control.structure.undoList.removeLastUndo(undoObj) super().accept() _pdfPrinterName = _('TreeLine PDF Printer') class GeneralPage(QWidget): """Dialog page for misc. print options. """ printerChanged = pyqtSignal(str) def __init__(self, printData, parent=None): """Create the general settings page. Arguments: printData -- a reference to the PrintData class parent -- the parent dialog """ super().__init__(parent) self.printData = printData self.printerList = QPrinterInfo.availablePrinterNames() self.printerList.insert(0, _pdfPrinterName) self.currentPrinterName = self.printData.printer.printerName() if not self.currentPrinterName: self.currentPrinterName = _pdfPrinterName topLayout = QHBoxLayout(self) self.setLayout(topLayout) leftLayout = QVBoxLayout() topLayout.addLayout(leftLayout) whatGroupBox = QGroupBox(_('What to print')) leftLayout.addWidget(whatGroupBox) whatLayout = QVBoxLayout(whatGroupBox) self.whatButtons = QButtonGroup(self) treeButton = QRadioButton(_('&Entire tree')) self.whatButtons.addButton(treeButton, printdata.PrintScope.entireTree) whatLayout.addWidget(treeButton) branchButton = QRadioButton(_('Selected &branches')) self.whatButtons.addButton(branchButton, printdata.PrintScope.selectBranch) whatLayout.addWidget(branchButton) nodeButton = QRadioButton(_('Selected &nodes')) self.whatButtons.addButton(nodeButton, printdata.PrintScope.selectNode) whatLayout.addWidget(nodeButton) self.whatButtons.button(self.printData.printWhat).setChecked(True) self.whatButtons.buttonClicked.connect(self.updateCmdAvail) includeBox = QGroupBox(_('Included Nodes')) leftLayout.addWidget(includeBox) includeLayout = QVBoxLayout(includeBox) self.rootButton = QCheckBox(_('&Include root node')) includeLayout.addWidget(self.rootButton) self.rootButton.setChecked(self.printData.includeRoot) self.openOnlyButton = QCheckBox(_('Onl&y open node children')) includeLayout.addWidget(self.openOnlyButton) self.openOnlyButton.setChecked(self.printData.openOnly) leftLayout.addStretch() rightLayout = QVBoxLayout() topLayout.addLayout(rightLayout) printerBox = QGroupBox(_('Select &Printer')) rightLayout.addWidget(printerBox) printerLayout = QVBoxLayout(printerBox) printerCombo = QComboBox() printerLayout.addWidget(printerCombo) printerCombo.addItems(self.printerList) printerCombo.setCurrentIndex(self.printerList.index(self. currentPrinterName)) printerCombo.currentIndexChanged.connect(self.changePrinter) featureBox = QGroupBox(_('Features')) rightLayout.addWidget(featureBox) featureLayout = QVBoxLayout(featureBox) self.linesButton = QCheckBox(_('&Draw lines to children')) featureLayout.addWidget(self.linesButton) self.linesButton.setChecked(self.printData.drawLines) self.widowButton = QCheckBox(_('&Keep first child with parent')) featureLayout.addWidget(self.widowButton) self.widowButton.setChecked(self.printData.widowControl) indentBox = QGroupBox(_('Indent')) rightLayout.addWidget(indentBox) indentLayout = QHBoxLayout(indentBox) indentLabel = QLabel(_('Indent Offse&t\n(line height units)')) indentLayout.addWidget(indentLabel) self.indentSpin = QDoubleSpinBox() indentLayout.addWidget(self.indentSpin) indentLabel.setBuddy(self.indentSpin) self.indentSpin.setMinimum(0.5) self.indentSpin.setSingleStep(0.5) self.indentSpin.setDecimals(1) self.indentSpin.setValue(self.printData.indentFactor) rightLayout.addStretch() self.updateCmdAvail() def updateCmdAvail(self): """Update options available based on print what settings. """ if self.whatButtons.checkedId() == printdata.PrintScope.selectNode: self.rootButton.setChecked(True) self.rootButton.setEnabled(False) self.openOnlyButton.setChecked(False) self.openOnlyButton.setEnabled(False) else: self.rootButton.setEnabled(True) self.openOnlyButton.setEnabled(True) def changePrinter(self, printerNum): """Change the current printer based on a combo box signal. Arguments: printerNum -- the printer number from the combo box """ self.currentPrinterName = self.printerList[printerNum] self.printerChanged.emit(self.currentPrinterName) def saveChanges(self): """Update print data with current dialog settings. Return True if saved settings have changed, False otherwise. """ self.printData.printWhat = self.whatButtons.checkedId() self.printData.includeRoot = self.rootButton.isChecked() self.printData.openOnly = self.openOnlyButton.isChecked() if self.currentPrinterName != _pdfPrinterName: self.printData.printer.setPrinterName(self.currentPrinterName) else: self.printData.printer.setPrinterName('') changed = False if self.printData.drawLines != self.linesButton.isChecked(): self.printData.drawLines = self.linesButton.isChecked() changed = True if self.printData.widowControl != self.widowButton.isChecked(): self.printData.widowControl = self.widowButton.isChecked() changed = True if self.printData.indentFactor != self.indentSpin.value(): self.printData.indentFactor = self.indentSpin.value() changed = True return changed _paperSizes = collections.OrderedDict([('Letter', _('Letter (8.5 x 11 in.)')), ('Legal', _('Legal (8.5 x 14 in.)'),), ('Tabloid', _('Tabloid (11 x 17 in.)')), ('A3', _('A3 (279 x 420 mm)')), ('A4', _('A4 (210 x 297 mm)')), ('A5', _('A5 (148 x 210 mm)')), ('Custom', _('Custom Size'))]) _units = collections.OrderedDict([('in', _('Inches (in)')), ('mm', _('Millimeters (mm)')), ('cm', _('Centimeters (cm)'))]) _unitValues = {'in': 1.0, 'cm': 2.54, 'mm': 25.4} _unitDecimals = {'in': 2, 'cm': 1, 'mm': 0} class PageSetupPage(QWidget): """Dialog page for page setup options. """ def __init__(self, printData, currentPrinterName, parent=None): """Create the page setup settings page. Arguments: printData -- a reference to the PrintData class currentPrinterName -- the selected printer for validation parent -- the parent dialog """ super().__init__(parent) self.printData = printData self.currentPrinterName = currentPrinterName topLayout = QHBoxLayout(self) self.setLayout(topLayout) leftLayout = QVBoxLayout() topLayout.addLayout(leftLayout) unitsBox = QGroupBox(_('&Units')) leftLayout.addWidget(unitsBox) unitsLayout = QVBoxLayout(unitsBox) unitsCombo = QComboBox() unitsLayout.addWidget(unitsCombo) unitsCombo.addItems(list(_units.values())) self.currentUnit = globalref.miscOptions['PrintUnits'] if self.currentUnit not in _units: self.currentUnit = 'in' unitsCombo.setCurrentIndex(list(_units.keys()).index(self.currentUnit)) unitsCombo.currentIndexChanged.connect(self.changeUnits) paperSizeBox = QGroupBox(_('Paper &Size')) leftLayout.addWidget(paperSizeBox) paperSizeLayout = QGridLayout(paperSizeBox) spacing = paperSizeLayout.spacing() paperSizeLayout.setVerticalSpacing(0) paperSizeLayout.setRowMinimumHeight(1, spacing) paperSizeCombo = QComboBox() paperSizeLayout.addWidget(paperSizeCombo, 0, 0, 1, 2) paperSizeCombo.addItems(list(_paperSizes.values())) self.currentPaperSize = self.printData.paperSizeName() if self.currentPaperSize not in _paperSizes: self.currentPaperSize = 'Custom' paperSizeCombo.setCurrentIndex(list(_paperSizes.keys()). index(self.currentPaperSize)) paperSizeCombo.currentIndexChanged.connect(self.changePaper) widthLabel = QLabel(_('&Width:')) paperSizeLayout.addWidget(widthLabel, 2, 0) self.paperWidthSpin = UnitSpinBox(self.currentUnit) paperSizeLayout.addWidget(self.paperWidthSpin, 3, 0) widthLabel.setBuddy(self.paperWidthSpin) paperWidth, paperHeight = self.printData.roundedPaperSize() self.paperWidthSpin.setInchValue(paperWidth) heightlabel = QLabel(_('Height:')) paperSizeLayout.addWidget(heightlabel, 2, 1) self.paperHeightSpin = UnitSpinBox(self.currentUnit) paperSizeLayout.addWidget(self.paperHeightSpin, 3, 1) heightlabel.setBuddy(self.paperHeightSpin) self.paperHeightSpin.setInchValue(paperHeight) if self.currentPaperSize != 'Custom': self.paperWidthSpin.setEnabled(False) self.paperHeightSpin.setEnabled(False) orientbox = QGroupBox(_('Orientation')) leftLayout.addWidget(orientbox) orientLayout = QVBoxLayout(orientbox) portraitButton = QRadioButton(_('Portra&it')) orientLayout.addWidget(portraitButton) landscapeButton = QRadioButton(_('Lan&dscape')) orientLayout.addWidget(landscapeButton) self.portraitOrient = (self.printData.pageLayout.orientation() == QPageLayout.Portrait) if self.portraitOrient: portraitButton.setChecked(True) else: landscapeButton.setChecked(True) portraitButton.toggled.connect(self.changeOrient) rightLayout = QVBoxLayout() topLayout.addLayout(rightLayout) marginsBox = QGroupBox(_('Margins')) rightLayout.addWidget(marginsBox) marginsLayout = QGridLayout(marginsBox) spacing = marginsLayout.spacing() marginsLayout.setVerticalSpacing(0) marginsLayout.setRowMinimumHeight(2, spacing) marginsLayout.setRowMinimumHeight(5, spacing) leftLabel = QLabel(_('&Left:')) marginsLayout.addWidget(leftLabel, 3, 0) leftMarginSpin = UnitSpinBox(self.currentUnit) marginsLayout.addWidget(leftMarginSpin, 4, 0) leftLabel.setBuddy(leftMarginSpin) topLabel = QLabel(_('&Top:')) marginsLayout.addWidget(topLabel, 0, 1) topMarginSpin = UnitSpinBox(self.currentUnit) marginsLayout.addWidget(topMarginSpin, 1, 1) topLabel.setBuddy(topMarginSpin) rightLabel = QLabel(_('&Right:')) marginsLayout.addWidget(rightLabel, 3, 2) rightMarginSpin = UnitSpinBox(self.currentUnit) marginsLayout.addWidget(rightMarginSpin, 4, 2) rightLabel.setBuddy(rightMarginSpin) bottomLabel = QLabel(_('&Bottom:')) marginsLayout.addWidget(bottomLabel, 6, 1) bottomMarginSpin = UnitSpinBox(self.currentUnit) marginsLayout.addWidget(bottomMarginSpin, 7, 1) bottomLabel.setBuddy(bottomMarginSpin) self.marginControls = (leftMarginSpin, topMarginSpin, rightMarginSpin, bottomMarginSpin) for control, value in zip(self.marginControls, self.printData.roundedMargins()): control.setInchValue(value) headerLabel = QLabel(_('He&ader:')) marginsLayout.addWidget(headerLabel, 0, 2) self.headerMarginSpin = UnitSpinBox(self.currentUnit) marginsLayout.addWidget(self.headerMarginSpin, 1, 2) headerLabel.setBuddy(self.headerMarginSpin) self.headerMarginSpin.setInchValue(self.printData.headerMargin) footerLabel = QLabel(_('Foot&er:')) marginsLayout.addWidget(footerLabel, 6, 2) self.footerMarginSpin = UnitSpinBox(self.currentUnit) marginsLayout.addWidget(self.footerMarginSpin, 7, 2) footerLabel.setBuddy(self.footerMarginSpin) self.footerMarginSpin.setInchValue(self.printData.footerMargin) columnsBox = QGroupBox(_('Columns')) rightLayout.addWidget(columnsBox) columnLayout = QGridLayout(columnsBox) numLabel = QLabel(_('&Number of columns')) columnLayout.addWidget(numLabel, 0, 0) self.columnSpin = QSpinBox() columnLayout.addWidget(self.columnSpin, 0, 1) numLabel.setBuddy(self.columnSpin) self.columnSpin.setMinimum(1) self.columnSpin.setMaximum(9) self.columnSpin.setValue(self.printData.numColumns) spaceLabel = QLabel(_('Space between colu&mns')) columnLayout.addWidget(spaceLabel, 1, 0) self.columnSpaceSpin = UnitSpinBox(self.currentUnit) columnLayout.addWidget(self.columnSpaceSpin, 1, 1) spaceLabel.setBuddy(self.columnSpaceSpin) self.columnSpaceSpin.setInchValue(self.printData.columnSpacing) def changePrinter(self, newPrinterName): """Change the currently selected printer. Arguments: newPrinterName -- new printer selection """ self.currentPrinterName = newPrinterName def changeUnits(self, unitNum): """Change the current unit and update conversions based on a signal. Arguments: unitNum -- the unit index number from the combobox """ oldUnit = self.currentUnit self.currentUnit = list(_units.keys())[unitNum] self.paperWidthSpin.changeUnit(self.currentUnit) self.paperHeightSpin.changeUnit(self.currentUnit) for control in self.marginControls: control.changeUnit(self.currentUnit) self.headerMarginSpin.changeUnit(self.currentUnit) self.footerMarginSpin.changeUnit(self.currentUnit) self.columnSpaceSpin.changeUnit(self.currentUnit) def changePaper(self, paperNum): """Change the current paper size based on a signal. Arguments: paperNum -- the paper size index number from the combobox """ self.currentPaperSize = list(_paperSizes.keys())[paperNum] if self.currentPaperSize != 'Custom': tempPrinter = QPrinter() pageLayout = tempPrinter.pageLayout() pageLayout.setPageSize(QPageSize(getattr(QPageSize, self.currentPaperSize))) if not self.portraitOrient: pageLayout.setOrientation(QPageLayout.Landscape) paperSize = pageLayout.fullRect(QPageLayout.Inch) self.paperWidthSpin.setInchValue(round(paperSize.width(), 2)) self.paperHeightSpin.setInchValue(round(paperSize.height(), 2)) self.paperWidthSpin.setEnabled(self.currentPaperSize == 'Custom') self.paperHeightSpin.setEnabled(self.currentPaperSize == 'Custom') def changeOrient(self, isPortrait): """Change the orientation based on a signal. Arguments: isPortrait -- true if portrait orientation is selected """ self.portraitOrient = isPortrait width = self.paperWidthSpin.inchValue height = self.paperHeightSpin.inchValue if (self.portraitOrient and width > height) or (not self.portraitOrient and width < height): self.paperWidthSpin.setInchValue(height) self.paperHeightSpin.setInchValue(width) def checkValid(self): """Return True if the current page size and margins appear to be valid. """ pageWidth = self.paperWidthSpin.inchValue pageHeight = self.paperHeightSpin.inchValue if pageWidth <= 0 or pageHeight <= 0: return False margins = tuple(control.inchValue for control in self.marginControls) if (margins[0] + margins[2] >= pageWidth or margins[1] + margins[3] >= pageHeight): return False return True def saveChanges(self): """Update print data with current dialog settings. Return True if saved settings have changed, False otherwise. """ if self.currentUnit != globalref.miscOptions['PrintUnits']: globalref.miscOptions.changeValue('PrintUnits', self.currentUnit) globalref.miscOptions.writeFile() changed = False pageLayout = self.printData.pageLayout if self.currentPaperSize != 'Custom': size = getattr(QPageSize, self.currentPaperSize) if size != pageLayout.pageSize().id(): pageLayout.setPageSize(QPageSize(size)) changed = True else: size = (self.paperWidthSpin.inchValue, self.paperHeightSpin.inchValue) if size != self.printData.roundedPaperSize(): pageLayout.setPageSize(QPageSize(QSizeF(*size), QPageSize.Inch)) changed = True orient = (QPageLayout.Portrait if self.portraitOrient else QPageLayout.Landscape) if orient != pageLayout.orientation(): pageLayout.setOrientation(orient) changed = True margins = tuple(control.inchValue for control in self.marginControls) if margins != self.printData.roundedMargins(): pageLayout.setMargins(QMarginsF(*margins)) changed = True if self.printData.headerMargin != self.headerMarginSpin.inchValue: self.printData.headerMargin = self.headerMarginSpin.inchValue changed = True if self.printData.footerMargin != self.footerMarginSpin.inchValue: self.printData.footerMargin = self.footerMarginSpin.inchValue changed = True if self.printData.numColumns != self.columnSpin.value(): self.printData.numColumns = self.columnSpin.value() changed = True if self.printData.columnSpacing != self.columnSpaceSpin.inchValue: self.printData.columnSpacing = self.columnSpaceSpin.inchValue changed = True return changed class UnitSpinBox(QDoubleSpinBox): """Spin box with unit suffix that can convert the units of its contents. Stores the value at full precision to avoid round-trip rounding errors. """ def __init__(self, unit, parent=None): """Create the unit spin box. Arguments: unit -- the original unit (abbreviated string) parent -- the parent dialog if given """ super().__init__(parent) self.unit = unit self.inchValue = 0.0 self.setupUnit() self.valueChanged.connect(self.changeValue) def setupUnit(self): """Set the suffix, decimal places and maximum based on the unit. """ self.blockSignals(True) self.setSuffix(' {0}'.format(self.unit)) decPlaces = _unitDecimals[self.unit] self.setDecimals(decPlaces) # set maximum to 5 digits total self.setMaximum((10**5 - 1) / 10**decPlaces) self.blockSignals(False) def changeUnit(self, unit): """Change current unit. Arguments: unit -- the new unit (abbreviated string) """ self.unit = unit self.setupUnit() self.setInchValue(self.inchValue) def setInchValue(self, inchValue): """Set box to given value, converted to current unit. Arguments: inchValue -- the value to set in inches """ self.inchValue = inchValue value = self.inchValue * _unitValues[self.unit] self.blockSignals(True) self.setValue(value) self.blockSignals(False) if value < 4: self.setSingleStep(0.1) elif value > 50: self.setSingleStep(10) else: self.setSingleStep(1) def changeValue(self): """Change the stored inch value based on a signal. """ self.inchValue = round(self.value() / _unitValues[self.unit], 2) class SmallListWidget(QListWidget): """ListWidget with a smaller size hint. """ def __init__(self, parent=None): """Initialize the widget. Arguments: parent -- the parent, if given """ super().__init__(parent) def sizeHint(self): """Return smaller width. """ itemHeight = self.visualItemRect(self.item(0)).height() return QSize(100, itemHeight * 3) class FontPage(QWidget): """Font selection print option dialog page. """ def __init__(self, printData, defaultLabel='', parent=None): """Create the font settings page. Arguments: printData -- a reference to the PrintData class defaultLabel -- default font label if given, o/w TreeLine output parent -- the parent dialog """ super().__init__(parent) self.printData = printData self.currentFont = self.printData.mainFont topLayout = QVBoxLayout(self) self.setLayout(topLayout) defaultBox = QGroupBox(_('Default Font')) topLayout.addWidget(defaultBox) defaultLayout = QVBoxLayout(defaultBox) if not defaultLabel: defaultLabel = _('&Use TreeLine output view font') self.defaultCheck = QCheckBox(defaultLabel) defaultLayout.addWidget(self.defaultCheck) self.defaultCheck.setChecked(self.printData.useDefaultFont) self.defaultCheck.clicked.connect(self.setFontSelectAvail) self.fontBox = QGroupBox(_('Select Font')) topLayout.addWidget(self.fontBox) fontLayout = QGridLayout(self.fontBox) spacing = fontLayout.spacing() fontLayout.setSpacing(0) label = QLabel(_('&Font')) fontLayout.addWidget(label, 0, 0) label.setIndent(2) self.familyEdit = 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 = [family for family in QFontDatabase().families()] families.sort(key=str.lower) self.familyList.addItems(families) self.familyList.currentItemChanged.connect(self.updateFamily) label = QLabel(_('Font st&yle')) fontLayout.addWidget(label, 0, 2) label.setIndent(2) self.styleEdit = 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.styleList.currentItemChanged.connect(self.updateStyle) label = QLabel(_('Si&ze')) fontLayout.addWidget(label, 0, 4) label.setIndent(2) self.sizeEdit = QLineEdit() fontLayout.addWidget(self.sizeEdit, 1, 4) self.sizeEdit.setFocusPolicy(Qt.ClickFocus) validator = QIntValidator(1, 512, self) self.sizeEdit.setValidator(validator) self.sizeList = SmallListWidget() fontLayout.addWidget(self.sizeList, 2, 4) label.setBuddy(self.sizeList) self.sizeList.currentItemChanged.connect(self.updateSize) fontLayout.setColumnStretch(0, 30) fontLayout.setColumnStretch(2, 25) fontLayout.setColumnStretch(4, 10) sampleBox = QGroupBox(_('Sample')) topLayout.addWidget(sampleBox) sampleLayout = QVBoxLayout(sampleBox) self.sampleEdit = QLineEdit() sampleLayout.addWidget(self.sampleEdit) self.sampleEdit.setAlignment(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 font is checked. Also set the controls with the current or default fonts. """ if self.defaultCheck.isChecked(): font = self.readFont() if font: self.currentFont = font self.setFont(self.printData.defaultFont) 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. Arguments: font -- the QFont to set. """ fontInfo = QFontInfo(font) family = fontInfo.family() matches = self.familyList.findItems(family, Qt.MatchExactly) if matches: self.familyList.setCurrentItem(matches[0]) self.familyList.scrollToItem(matches[0], QAbstractItemView.PositionAtTop) style = QFontDatabase().styleString(fontInfo) matches = self.styleList.findItems(style, Qt.MatchExactly) if matches: self.styleList.setCurrentItem(matches[0]) self.styleList.scrollToItem(matches[0]) else: self.styleList.setCurrentRow(0) self.styleList.scrollToItem(self.styleList.currentItem()) size = repr(fontInfo.pointSize()) matches = self.sizeList.findItems(size, 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. Arguments: currentItem -- the new list widget family item previousItem -- the previous list widget item """ family = currentItem.text() self.familyEdit.setText(family) if self.familyEdit.hasFocus(): self.familyEdit.selectAll() prevStyle = self.styleEdit.text() prevSize = self.sizeEdit.text() fontDb = QFontDatabase() styles = [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. Arguments: currentItem -- the new list widget style item previousItem -- the previous list widget item """ if currentItem: style = 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. Arguments: currentItem -- the new list widget size item previousItem -- the previous list widget item """ if currentItem: size = 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 = self.familyEdit.text() style = self.styleEdit.text() size = self.sizeEdit.text() if family and style and size: return QFontDatabase().font(family, style, int(size)) return None def saveChanges(self): """Update print data with current dialog settings. Return True if saved settings have changed, False otherwise. """ if self.defaultCheck.isChecked(): if not self.printData.useDefaultFont: self.printData.useDefaultFont = True self.printData.mainFont = self.printData.defaultFont return True else: font = self.readFont() if font and (self.printData.useDefaultFont or font != self.printData.mainFont): self.printData.useDefaultFont = False self.printData.mainFont = font return True return False _headerNames = (_('&Header Left'), _('Header C&enter'), _('Header &Right')) _footerNames = (_('Footer &Left'), _('Footer Ce&nter'), _('Footer Righ&t')) class HeaderPage(QWidget): """Header/footer print option dialog page. """ def __init__(self, printData, parent=None): """Create the header/footer settings page. Arguments: printData -- a reference to the PrintData class parent -- the parent dialog """ super().__init__(parent) self.printData = printData self.focusedEditor = None topLayout = QGridLayout(self) fieldBox = QGroupBox(_('Fiel&ds')) topLayout.addWidget(fieldBox, 0, 0, 3, 1) fieldLayout = QVBoxLayout(fieldBox) self.fieldListWidget = FieldListWidget() fieldLayout.addWidget(self.fieldListWidget) fieldFormatButton = QPushButton(_('Field For&mat')) fieldLayout.addWidget(fieldFormatButton) fieldFormatButton.clicked.connect(self.showFieldFormatDialog) self.addFieldButton = QPushButton('>>') topLayout.addWidget(self.addFieldButton, 0, 1) self.addFieldButton.setMaximumWidth(self.addFieldButton.sizeHint(). height()) self.addFieldButton.clicked.connect(self.addField) self.delFieldButton = QPushButton('<<') topLayout.addWidget(self.delFieldButton, 1, 1) self.delFieldButton.setMaximumWidth(self.delFieldButton.sizeHint(). height()) self.delFieldButton.clicked.connect(self.delField) headerFooterBox = QGroupBox(_('Header and Footer')) topLayout.addWidget(headerFooterBox, 0, 2, 2, 1) headerFooterLayout = QGridLayout(headerFooterBox) spacing = headerFooterLayout.spacing() headerFooterLayout.setVerticalSpacing(0) headerFooterLayout.setRowMinimumHeight(2, spacing) self.headerEdits = self.addLineEdits(_headerNames, headerFooterLayout, 0) self.footerEdits = self.addLineEdits(_footerNames, headerFooterLayout, 3) self.loadContent() def addLineEdits(self, names, layout, startRow): """Add line edits for header or footer. Return a list of line edits added to the top layout. Arguments: names -- a list of label names layout -- the grid layout t use startRow -- the initial row number """ lineEdits = [] for num, name in enumerate(names): label = QLabel(name) layout.addWidget(label, startRow, num) lineEdit = configdialog.TitleEdit() layout.addWidget(lineEdit, startRow + 1, num) label.setBuddy(lineEdit) lineEdit.cursorPositionChanged.connect(self.setControlAvailability) lineEdit.focusIn.connect(self.setCurrentEditor) lineEdits.append(lineEdit) return lineEdits def loadContent(self): """Load field names and header/footer text into the controls. """ self.fieldListWidget.addItems(self.printData.localControl.structure. treeFormats.fileInfoFormat.fieldNames()) self.fieldListWidget.setCurrentRow(0) for text, lineEdit in zip(splitHeaderFooter(self.printData.headerText), self.headerEdits): lineEdit.blockSignals(True) lineEdit.setText(text) lineEdit.blockSignals(False) for text, lineEdit in zip(splitHeaderFooter(self.printData.footerText), self.footerEdits): lineEdit.blockSignals(True) lineEdit.setText(text) lineEdit.blockSignals(False) self.focusedEditor = self.headerEdits[0] self.headerEdits[0].setFocus() self.setControlAvailability() def setControlAvailability(self): """Set controls available based on text cursor movements. """ cursorInField = self.isCursorInField() self.addFieldButton.setEnabled(cursorInField == None) self.delFieldButton.setEnabled(cursorInField == True) def setCurrentEditor(self, sender): """Set focusedEditor based on editor focus change signal. Arguments: sender -- the line editor to focus """ self.focusedEditor = sender self.setControlAvailability() def isCursorInField(self, selectField=False): """Return True if a field pattern encloses the cursor/selection. Return False if the selection overlaps a field. Return None if there is no field at the cursor. Arguments: selectField -- select the entire field pattern if True. """ cursorPos = self.focusedEditor.cursorPosition() selectStart = self.focusedEditor.selectionStart() if selectStart < 0: selectStart = cursorPos elif selectStart == cursorPos: # backward selection cursorPos += len(self.focusedEditor.selectedText()) textLine = self.focusedEditor.text() for match in configdialog.fieldPattern.finditer(textLine): start = (match.start() if match.start() < selectStart < match.end() else None) end = (match.end() if match.start() < cursorPos < match.end() else None) if start != None and end != None: if selectField: self.focusedEditor.setSelection(start, end - start) return True if start != None or end != None: return False return None def addField(self): """Add selected field to cursor pos in current line editor. """ fieldName = self.fieldListWidget.currentItem().text() self.focusedEditor.insert('{{*!{0}*}}'.format(fieldName)) self.focusedEditor.setFocus() def delField(self): """Remove field from cursor pos in current line editor. """ if self.isCursorInField(True): self.focusedEditor.insert('') self.focusedEditor.setFocus() def showFieldFormatDialog(self): """Show thw dialog used to set file info field formats. """ fileInfoFormat = (self.printData.localControl.structure.treeFormats. fileInfoFormat) fieldName = self.fieldListWidget.currentItem().text() field = fileInfoFormat.fieldDict[fieldName] dialog = HeaderFieldFormatDialog(field, self.printData.localControl, self) dialog.exec_() def saveChanges(self): """Update print data with current dialog settings. Return True if saved settings have changed, False otherwise. """ changed = False headerList = [lineEdit.text().replace('/', r'\/') for lineEdit in self.headerEdits] while len(headerList) > 1 and not headerList[-1]: del headerList[-1] text = '/'.join(headerList) if self.printData.headerText != text: self.printData.headerText = text changed = True footerList = [lineEdit.text().replace('/', r'\/') for lineEdit in self.footerEdits] while len(footerList) > 1 and not footerList[-1]: del footerList[-1] text = '/'.join(footerList) if self.printData.footerText != text: self.printData.footerText = text changed = True return changed class FieldListWidget(QListWidget): """List widget for fields with smaller width size hint. """ def __init__(self, parent=None): """Create the list widget. Arguments: parent -- the parent dialog """ super().__init__(parent) def sizeHint(self): """Return a size with a smaller width. """ return QSize(120, 100) class HeaderFieldFormatDialog(QDialog): """Dialog to modify file info field formats used in headers and footers. """ def __init__(self, field, localControl, parent=None): """Create the field format dialog. Arguments: field -- the field to be modified localControl -- a ref to the control to save changes and undo """ super().__init__(parent) self.field = field self.localControl = localControl self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Field Format for "{0}"').format(field.name)) topLayout = QVBoxLayout(self) self.setLayout(topLayout) self.formatBox = QGroupBox(_('Output &Format')) topLayout.addWidget(self.formatBox) formatLayout = QHBoxLayout(self.formatBox) self.formatEdit = QLineEdit() formatLayout.addWidget(self.formatEdit) self.helpButton = QPushButton(_('Format &Help')) formatLayout.addWidget(self.helpButton) self.helpButton.clicked.connect(self.formatHelp) extraBox = QGroupBox(_('Extra Text')) topLayout.addWidget(extraBox) extraLayout = QVBoxLayout(extraBox) spacing = extraLayout.spacing() extraLayout.setSpacing(0) prefixLabel = QLabel(_('&Prefix')) extraLayout.addWidget(prefixLabel) self.prefixEdit = QLineEdit() extraLayout.addWidget(self.prefixEdit) prefixLabel.setBuddy(self.prefixEdit) extraLayout.addSpacing(spacing) suffixLabel = QLabel(_('&Suffix')) extraLayout.addWidget(suffixLabel) self.suffixEdit = QLineEdit() extraLayout.addWidget(self.suffixEdit) suffixLabel.setBuddy(self.suffixEdit) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch() okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.prefixEdit.setText(self.field.prefix) self.suffixEdit.setText(self.field.suffix) self.formatEdit.setText(self.field.format) self.formatBox.setEnabled(self.field.defaultFormat != '') def formatHelp(self): """Provide a format help menu based on a button signal. """ menu = QMenu(self) self.formatHelpDict = {} for descript, key in self.field.getFormatHelpMenuList(): if descript: self.formatHelpDict[descript] = key menu.addAction(descript) else: menu.addSeparator() menu.popup(self.helpButton. mapToGlobal(QPoint(0, self.helpButton.height()))) menu.triggered.connect(self.insertFormat) def insertFormat(self, action): """Insert format text from help menu into edit box. Arguments: action -- the action from the help menu """ self.formatEdit.insert(self.formatHelpDict[action.text()]) def accept(self): """Set changes after OK is hit""" prefix = self.prefixEdit.text() suffix = self.suffixEdit.text() format = self.formatEdit.text() if (self.field.prefix != prefix or self.field.suffix != suffix or self.field.format != format): undo.FormatUndo(self.localControl.structure.undoList, self.localControl.structure.treeFormats, treeformats.TreeFormats()) self.field.prefix = prefix self.field.suffix = suffix self.field.format = format self.localControl.setModified() super().accept() _headerSplitRe = re.compile(r'(?')) _origBackrefMatch = None class TreeNode: """Class to store tree node data and the tree's linked structure. Stores a data dict, lists of children and a format name string. Provides methods to get info on the structure and the data. """ def __init__(self, formatRef, fileData=None): """Initialize a tree node. Arguments: formatRef -- a ref to this node's format info fileData -- a dict with uid, data, child refs & parent refs """ self.formatRef = formatRef if not fileData: fileData = {} self.uId = fileData.get('uid', uuid.uuid1().hex) self.data = fileData.get('data', {}) self.tmpChildRefs = fileData.get('children', []) self.childList = [] self.spotRefs = set() def assignRefs(self, nodeDict): """Add actual refs to child nodes from data in self.tmpChildRefs. Any bad node refs (corrupt file data) are left in self.tmpChildRefs. Arguments: nodeDict -- all nodes stored by uid """ try: self.childList = [nodeDict[uid] for uid in self.tmpChildRefs] self.tmpChildRefs = [] except KeyError: # due to corrupt file data badChildRefs = [] for uid in self.tmpChildRefs: if uid in nodeDict: self.childList.append(nodeDict[uid]) else: badChildRefs.append(uid) self.tmpChildRefs = badChildRefs def generateSpots(self, parentSpot): """Recursively generate spot references for this branch. Arguments: parentSpot -- the parent spot reference """ spot = treespot.TreeSpot(self, parentSpot) self.spotRefs.add(spot) for child in self.childList: child.generateSpots(spot) def addSpotRef(self, parentNode, includeChildren=True): """Add a spot ref here to the given parent if not already there. If changed, propogate to descendant nodes. Arguments: parentNode -- the parent to ref in the new spot includeChildren -- if True, propogate to descendant nodes """ changed = False origParentSpots = {spot.parentSpot for spot in self.spotRefs} for parentSpot in parentNode.spotRefs: if parentSpot not in origParentSpots: self.spotRefs.add(treespot.TreeSpot(self, parentSpot)) changed = True if changed and includeChildren: for child in self.childList: child.addSpotRef(self) def removeInvalidSpotRefs(self, includeChildren=True, forceDesend=False): """Verify existing spot refs and remove any that aren't valid. If changed and includeChilderen, propogate to descendant nodes. Arguments: includeChildren -- if True, propogate to descendants if changes forceDesend -- if True, force propogate to descendant nodes """ goodSpotRefs = {spot for spot in self.spotRefs if (self in spot.parentSpot.nodeRef.childList and spot.parentSpot in spot.parentSpot.nodeRef.spotRefs)} changed = len(self.spotRefs) != len(goodSpotRefs) self.spotRefs = goodSpotRefs if includeChildren and (changed or forceDesend): for child in self.childList: child.removeInvalidSpotRefs(includeChildren) def spotByNumber(self, num): """Return the spot at the given rank in the spot sequence. Arguments: num -- the rank number to return """ spotList = sorted(list(self.spotRefs), key=operator.methodcaller('sortKey')) return spotList[num] def matchedSpot(self, parentSpot): """Return the spot for this node that matches a parent spot. Return None if not found. Arguments: parentSpot -- the parent to match """ for spot in self.spotRefs: if spot.parentSpot is parentSpot: return spot return None def setInitDefaultData(self, overwrite=False): """Add initial default data from fields into internal data. Arguments: overwrite -- if true, replace previous data entries """ self.formatRef.setInitDefaultData(self.data, overwrite) def parents(self): """Return a set of parent nodes for this node. Returns an empty set if called from the tree structure. """ try: return {spot.parentSpot.nodeRef for spot in self.spotRefs} except AttributeError: return set() def numChildren(self): """Return number of children. """ return len(self.childList) def descendantGen(self): """Return a generator to step through all nodes in this branch. Includes self and closed nodes. """ yield self for child in self.childList: for node in child.descendantGen(): yield node def ancestors(self): """Return a set of all ancestor nodes (including self). """ spots = set() for spot in self.spotRefs: spots.update(spot.spotChain()) return {spot.nodeRef for spot in spots} def treeStructureRef(self): """Return the tree structure based on the root spot ref. """ return next(iter(self.spotRefs)).rootSpot().nodeRef def fileData(self): """Return the file data dict for this node. """ children = [node.uId for node in self.childList] fileData = {'format': self.formatRef.name, 'uid': self.uId, 'data': self.data, 'children': children} return fileData def title(self, spotRef=None): """Return the title string for this node. If spotRef not given, ancestor fields assume first spot. Arguments: spotRef -- optional, used for ancestor field refs """ return self.formatRef.formatTitle(self, spotRef) def setTitle(self, title): """Change this node's data based on a new title string. Return True if successfully changed. """ if title == self.title(): return False return self.formatRef.extractTitleData(title, self.data) def output(self, plainText=False, keepBlanks=False, spotRef=None): """Return a list of formatted text output lines. If spotRef not given, ancestor fields assume first spot. Arguments: plainText -- if True, remove HTML markup from fields and formats keepBlanks -- if True, keep lines with empty fields spotRef -- optional, used for ancestor field refs """ return self.formatRef.formatOutput(self, plainText, keepBlanks, spotRef) def changeDataType(self, formatRef): """Change this node's data type to the given name. Set init default data and update the title if blank. Arguments: formatRef -- the new tree format type """ origTitle = self.title() self.formatRef = formatRef formatRef.setInitDefaultData(self.data) if not formatRef.formatTitle(self): formatRef.extractTitleData(origTitle, self.data) def setConditionalType(self, treeStructure): """Set self to type based on auto conditional settings. Return True if type is changed. Arguments: treeStructure -- a ref to the tree structure """ if self.formatRef not in treeStructure.treeFormats.conditionalTypes: return False if self.formatRef.genericType: genericFormat = treeStructure.treeFormats[self.formatRef. genericType] else: genericFormat = self.formatRef formatList = [genericFormat] + genericFormat.derivedTypes formatList.remove(self.formatRef) formatList.insert(0, self.formatRef) # reorder to give priority neutralResult = None newType = None for typeFormat in formatList: if typeFormat.conditional: if typeFormat.conditional.evaluate(self): newType = typeFormat break elif not neutralResult: neutralResult = typeFormat if not newType and neutralResult: newType = neutralResult if newType and newType is not self.formatRef: self.changeDataType(newType) return True return False def setDescendantConditionalTypes(self, treeStructure): """Set auto conditional types for self and all descendants. Return number of changes made. Arguments: treeStructure -- a ref to the tree structure """ if not treeStructure.treeFormats.conditionalTypes: return 0 changes = 0 for node in self.descendantGen(): if node.setConditionalType(treeStructure): changes += 1 return changes def setData(self, field, editorText): """Set the data entry for the given field to editorText. If the data does not match the format, sets to the raw text and raises a ValueError. Arguments: field-- the field object to be set editorText -- new text data from an editor """ try: self.data[field.name] = field.storedText(editorText) except ValueError as err: if len(err.args) >= 2: self.data[field.name] = err.args[1] else: self.data[field.name] = editorText raise ValueError def wordSearch(self, wordList, titleOnly=False, spotRef=None): """Return True if all words in wordlist are found in this node's data. Arguments: wordList -- a list of words or phrases to find titleOnly -- search only in the title text if True spotRef -- an optional spot reference for ancestor field refs """ dataStr = self.title(spotRef).lower() if not titleOnly: # join with null char so phrase matches don't cross borders dataStr = '{0}\0{1}'.format(dataStr, '\0'.join(self.data.values()).lower()) for word in wordList: if word not in dataStr: return False return True def regExpSearch(self, regExpList, titleOnly=False, spotRef=None): """Return True if the regular expression is found in this node's data. Arguments: regExpList -- a list of regular expression objects to find titleOnly -- search only in the title text if True spotRef -- an optional spot reference for ancestor field refs """ dataStr = self.title(spotRef) if not titleOnly: # join with null char so phrase matches don't cross borders dataStr = '{0}\0{1}'.format(dataStr, '\0'.join(self.data.values())) for regExpObj in regExpList: if not regExpObj.search(dataStr): return False return True def searchReplace(self, searchText='', regExpObj=None, skipMatches=0, typeName='', fieldName='', replaceText=None, replaceAll=False): """Find the search text in the field data and optionally replace it. Returns a tuple of the fieldName where found (empty string if not found), the node match number and the field match number. Returns the last match if skipMatches < 0 (not used with replace). Arguments: searchText -- the text to find if regExpObj is None regExpObj -- the regular expression to find if not None skipMatches -- number of already found matches to skip in this node typeName -- if given, verify that this node matches this type fieldName -- if given, only find matches under this type name replaceText -- if not None, replace a match with this string replaceAll -- if True, replace all matches (returns last fieldName) """ if typeName and typeName != self.formatRef.name: return ('', 0, 0) try: fields = ([self.formatRef.fieldDict[fieldName]] if fieldName else self.formatRef.fields()) except KeyError: return ('', 0, 0) # field not in this type matchedFieldname = '' findCount = 0 prevFieldFindCount = 0 for field in fields: try: fieldText = field.editorText(self) except ValueError: fieldText = self.data.get(field.name, '') fieldFindCount = 0 pos = 0 while True: if pos >= len(fieldText) and pos > 0: break if regExpObj: match = regExpObj.search(fieldText, pos) pos = match.start() if match else -1 else: pos = fieldText.lower().find(searchText, pos) if not searchText and fieldText: pos = -1 # skip invalid find of empty string if pos < 0: break findCount += 1 fieldFindCount += 1 prevFieldFindCount = fieldFindCount matchLen = (len(match.group()) if regExpObj else len(searchText)) if findCount > skipMatches: matchedFieldname = field.name if replaceText is not None: replace = replaceText if regExpObj: global _origBackrefMatch _origBackrefMatch = match for backrefRe in _replaceBackrefRe: replace = backrefRe.sub(self.replaceBackref, replace) fieldText = (fieldText[:pos] + replace + fieldText[pos + matchLen:]) try: self.setData(field, fieldText) except ValueError: pass if not replaceAll and skipMatches >= 0: return (field.name, findCount, fieldFindCount) pos = pos + matchLen if matchLen else pos + 1 if not matchedFieldname: findCount = prevFieldFindCount = 0 return (matchedFieldname, findCount, prevFieldFindCount) @staticmethod def replaceBackref(match): """Return the re match group from _origBackrefMatch for replacement. Used for reg exp backreference replacement. Arguments: match -- the backref match in the replacement string """ return _origBackrefMatch.group(int(match.group(1))) def addNewChild(self, treeStructure, posRefNode=None, insertBefore=True, newTitle=_('New')): """Add a new child node with this node as the parent. Insert the new node near the posRefNode or at the end if no ref node. Return the new node. Arguments: treeStructure -- a ref to the tree structure posRefNode -- a child reference for the new node's position insertBefore -- insert before the ref node if True, after if False """ try: newFormat = treeStructure.treeFormats[self.formatRef.childType] except (KeyError, AttributeError): if posRefNode: newFormat = posRefNode.formatRef elif self.childList: newFormat = self.childList[0].formatRef else: newFormat = self.formatRef newNode = TreeNode(newFormat) pos = len(self.childList) if posRefNode: pos = self.childList.index(posRefNode) if not insertBefore: pos += 1 self.childList.insert(pos, newNode) newNode.setInitDefaultData() newNode.addSpotRef(self) if newTitle and not newNode.title(): newNode.setTitle(newTitle) treeStructure.addNodeDictRef(newNode) return newNode def changeParent(self, oldParentSpot, newParentSpot, newPos=-1): """Move this node from oldParent to newParent. Used for indent and unindent commands. Arguments: oldParent -- the original parent spot newParent -- the new parent spot newPos -- the position in the new childList, -1 for append """ oldParent = oldParentSpot.nodeRef oldParent.childList.remove(self) newParent = newParentSpot.nodeRef if newPos >= 0: newParent.childList.insert(newPos, self) else: newParent.childList.append(self) # preserve one spot to maintain tree expand state self.matchedSpot(oldParentSpot).parentSpot = newParentSpot self.removeInvalidSpotRefs() self.addSpotRef(newParent) def replaceChildren(self, titleList, treeStructure): """Replace child nodes with titles from a text list. Nodes with matches in the titleList are kept, others are added or deleted as required. Arguments: titleList -- the list of new child titles treeStructure -- a ref to the tree structure """ try: newFormat = treeStructure.treeFormats[self.formatRef.childType] except (KeyError, AttributeError): newFormat = (self.childList[0].formatRef if self.childList else self.formatRef) matchList = [] remainTitles = [child.title() for child in self.childList] for title in titleList: try: match = self.childList.pop(remainTitles.index(title)) matchList.append((title, match)) remainTitles = [child.title() for child in self.childList] except ValueError: matchList.append((title, None)) newChildList = [] firstMiss = True for title, node in matchList: if not node: if (firstMiss and remainTitles and remainTitles[0].startswith(title)): # accept partial match on first miss for split tiles node = self.childList.pop(0) node.setTitle(title) else: node = TreeNode(newFormat) node.setTitle(title) node.setInitDefaultData() node.addSpotRef(self) treeStructure.addNodeDictRef(node) firstMiss = False newChildList.append(node) for child in self.childList: for oldNode in child.descendantGen(): if len(oldNode.spotRefs) <= 1: treeStructure.removeNodeDictRef(oldNode) else: oldNode.removeInvalidSpotRefs(False) self.childList = newChildList def replaceClonedBranches(self, origStruct): """Replace any duplicate IDs with clones from the given structure. Recursively search for duplicates. Arguments: origStruct -- the tree structure with the cloned nodes """ for i in range(len(self.childList)): if self.childList[i].uId in origStruct.nodeDict: self.childList[i] = origStruct.nodeDict[self.childList[i].uId] else: self.childList[i].replaceClonedBranches(origStruct) def loadChildNodeLevels(self, nodeList, initLevel=-1): """Recursively add children from a list of nodes and levels. Return True on success, False if data levels are not valid. Arguments: nodeList -- list of tuples with node and level initLevel -- the level of this node in the structure """ while nodeList: child, level = nodeList[0] if level == initLevel + 1: del nodeList[0] self.childList.append(child) if not child.loadChildNodeLevels(nodeList, level): return False else: return -1 < level <= initLevel return True def fieldSortKey(self, level=0): """Return a key used to sort by key fields. Arguments: level -- the sort key depth level for the current sort stage """ if len(self.formatRef.sortFields) > level: return self.formatRef.sortFields[level].sortKey(self) return ('',) def sortChildrenByField(self, recursive=True, forward=True): """Sort child nodes by predefined field keys. Arguments: recursive -- continue to sort recursively if true forward -- reverse the sort if false """ formats = set([child.formatRef for child in self.childList]) maxDepth = 0 directions = [] for nodeFormat in formats: if not nodeFormat.sortFields: nodeFormat.loadSortFields() maxDepth = max(maxDepth, len(nodeFormat.sortFields)) newDirections = [field.sortKeyForward for field in nodeFormat.sortFields] directions = [sum(i) for i in itertools.zip_longest(directions, newDirections, fillvalue= False)] if forward: directions = [bool(direct) for direct in directions] else: directions = [not bool(direct) for direct in directions] for level in range(maxDepth, 0, -1): self.childList.sort(key = operator.methodcaller('fieldSortKey', level - 1), reverse = not directions[level - 1]) if recursive: for child in self.childList: child.sortChildrenByField(True, forward) def titleSortKey(self): """Return a key used to sort by titles. """ return self.title().lower() def sortChildrenByTitle(self, recursive=True, forward=True): """Sort child nodes by titles. Arguments: recursive -- continue to sort recursively if true forward -- reverse the sort if false """ self.childList.sort(key = operator.methodcaller('titleSortKey'), reverse = not forward) if recursive: for child in self.childList: child.sortChildrenByTitle(True, forward) def updateNodeMathFields(self, treeFormats): """Recalculate math fields that depend on this node and so on. Return True if any data was changed. Arguments: treeFormats -- a ref to all of the formats """ changed = False for field in self.formatRef.fields(): for fieldRef in treeFormats.mathFieldRefDict.get(field.name, []): for node in fieldRef.dependentEqnNodes(self): if node.recalcMathField(fieldRef.eqnFieldName, treeFormats): changed = True return changed def recalcMathField(self, eqnFieldName, treeFormats): """Recalculate a math field, if changed, recalc depending math fields. Return True if any data was changed. Arguments: eqnFieldName -- the equation field in this node to update treeFormats -- a ref to all of the formats """ changed = False oldValue = self.data.get(eqnFieldName, '') newValue = self.formatRef.fieldDict[eqnFieldName].equationValue(self) if newValue != oldValue: self.data[eqnFieldName] = newValue changed = True for fieldRef in treeFormats.mathFieldRefDict.get(eqnFieldName, []): for node in fieldRef.dependentEqnNodes(self): node.recalcMathField(fieldRef.eqnFieldName, treeFormats) return changed def updateNumbering(self, fieldDict, currentSequence, levelLimit, completedClones, includeRoot=True, reserveNums=True, restartSetting=False): """Add auto incremented numbering to fields by type in the dict. Arguments: fieldDict -- numbering field name lists stored by type name currentSequence -- a list of int for the current numbering sequence levelLimit -- the number of child levels to include completedClones -- set of clone nodes already numbered includeRoot -- if Ture, number the current node reserveNums -- if true, increment number even without num field restartSetting -- if true, restart numbering after a no-field gap """ childSequence = currentSequence[:] if includeRoot: for fieldName in fieldDict.get(self.formatRef.name, []): self.data[fieldName] = '.'.join((repr(num) for num in currentSequence)) if self.formatRef.name in fieldDict or reserveNums: childSequence += [1] currentSequence[-1] += 1 if restartSetting and self.formatRef.name not in fieldDict: currentSequence[-1] = 1 if len(self.spotRefs) > 1: completedClones.add(self.uId) if levelLimit > 0: for child in self.childList: if len(child.spotRefs) > 1 and child.uId in completedClones: return child.updateNumbering(fieldDict, childSequence, levelLimit - 1, completedClones, True, reserveNums, restartSetting) def isIdentical(self, node, checkParents=True): """Return True if node format, data and descendants are identical. Also returns False if checkParents & the nodes have parents in common. Arguments: node -- the node to check """ if (self.formatRef != node.formatRef or len(self.childList) != len(node.childList) or self.data != node.data or (checkParents and not self.parents().isdisjoint(node.parents()))): return False for thisChild, otherChild in zip(self.childList, node.childList): if not thisChild.isIdentical(otherChild, False): return False return True def flatChildCategory(self, origFormats, structure): """Collapse descendant nodes by merging fields. Overwrites data in any fields with the same name. Arguments: origFormats -- copy of tree formats before any changes structure -- a ref to the tree structure """ thisSpot = self.spotByNumber(0) newChildList = [] for spot in thisSpot.spotDescendantOnlyGen(): if not spot.nodeRef.childList: oldParentSpot = spot.parentSpot while oldParentSpot != thisSpot: for field in origFormats[oldParentSpot.nodeRef.formatRef. name].fields(): data = oldParentSpot.nodeRef.data.get(field.name, '') if data: spot.nodeRef.data[field.name] = data spot.nodeRef.formatRef.addFieldIfNew(field.name, field.formatData()) oldParentSpot = oldParentSpot.parentSpot spot.parentSpot = thisSpot newChildList.append(spot.nodeRef) else: structure.removeNodeDictRef(spot.nodeRef) self.childList = newChildList def addChildCategory(self, catList, structure): """Insert category nodes above children. Arguments: catList -- the field names to add to the new level structure -- a ref to the tree structure """ newFormat = None catSet = set(catList) similarFormats = [nodeFormat for nodeFormat in structure.treeFormats.values() if catSet.issubset(set(nodeFormat.fieldNames()))] if similarFormats: similarFormat = min(similarFormats, key=lambda f: len(f.fieldDict)) if len(similarFormat.fieldDict) < len(self.childList[0]. formatRef.fieldDict): newFormat = similarFormat if not newFormat: newFormatName = '{0}_TYPE'.format(catList[0].upper()) num = 1 while newFormatName in structure.treeFormats: newFormatName = '{0}_TYPE_{1}'.format(catList[0].upper(), num) num += 1 newFormat = nodeformat.NodeFormat(newFormatName, structure.treeFormats) newFormat.addFieldList(catList, True, True) structure.treeFormats[newFormatName] = newFormat newParents = [] for child in self.childList: newParent = child.findEqualFields(catList, newParents) if not newParent: newParent = TreeNode(newFormat) for field in catList: data = child.data.get(field, '') if data: newParent.data[field] = data structure.addNodeDictRef(newParent) newParents.append(newParent) newParent.childList.append(child) self.childList = newParents for child in self.childList: child.removeInvalidSpotRefs(True, True) child.addSpotRef(self) def findEqualFields(self, fieldNames, nodes): """Return first node in nodes with same data in fieldNames as self. Arguments: fieldNames -- the list of fields to check nodes -- the nodes to search for a match """ for node in nodes: for field in fieldNames: if self.data.get(field, '') != node.data.get(field, ''): break else: # this for loop didn't hit break, so we have a match return node def exportTitleText(self, level=0): """Return a list of tabbed title lines for this node and descendants. Arguments: level -- indicates the indent level needed """ textList = ['\t' * level + self.title()] for child in self.childList: textList.extend(child.exportTitleText(level + 1)) return textList TreeLine/source/genboolean.py0000644000175000017500000000766113433405374015246 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # genboolean.py, provides a class for boolean formating # # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re _formatDict = {N_('true'):True, N_('false'):False, N_('yes'):True, N_('no'):False} for key, value in _formatDict.copy().items(): _formatDict[key[0]] = value _formatDict[_(key)] = value _formatDict[_(key)[0]] = value class GenBoolean: """Class to store & format boolean values. Uses a simple format of /. """ def __init__(self, boolStr='true'): """Initialize a GenBoolean object with any format from _formatDict. Raises ValueError with an inappropriate argument. Arguments: boolStr -- the string to evaluate """ self.setBool(boolStr) def setBool(self, boolStr): """Initialize a GenBoolean object with any format from _formatDict. Raises ValueError with an inappropriate argument. Arguments: boolStr -- the string to evaluate """ try: self.value = _formatDict[boolStr.lower()] except KeyError: raise ValueError def setFromStr(self, boolStr, strFormat='yes/no'): """Set boolean value based on given format string. Raises ValueError with an inappropriate argument. Returns self. Arguments: boolStr -- the string to evaluate strFormat -- a text format in True/False style """ try: self.value = self.customFormatDict(strFormat)[boolStr.lower()] except KeyError: raise ValueError return self @staticmethod def customFormatDict(strFormat): """Return a dictionary based on the format. The dictionary includes conversions in both directions. String keys are in lower case. Double editSep's are not split (become single). Raises ValueError with an inappropriate format. Arguments: strFormat -- a text format in True/False style """ strFormat = strFormat.replace('//', '\0') trueVal, falseVal = strFormat.split('/', 1) trueVal = trueVal.replace('\0', '/') falseVal = falseVal.replace('\0', '/') if not trueVal or not falseVal or trueVal == falseVal: raise ValueError return {trueVal.lower():True, falseVal.lower():False, True:trueVal, False:falseVal} def boolStr(self, strFormat='yes/no'): """Return the boolean string in the given strFormat. Arguments: Format: strFormat -- a text format in True/False style """ return self.customFormatDict(strFormat)[self.value] def clone(self): """Return cloned instance. """ return self.__class__(self.value) def __repr__(self): """Outputs in general string fomat. """ return repr(self.value) def __eq__(self, other): """Equality test. """ try: return self.value == other.value except AttributeError: return self.value == other def __ne__(self, other): """Non-equality test. """ try: return self.value != other.value except AttributeError: return self.value != other def __hash__(self): """Allow use as dictionary key. """ return hash(self.value) def __nonzero(self): """Allow truth testing. """ return self.value TreeLine/source/numbering.py0000644000175000017500000001555313705103332015111 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # numbering.py, provides classes to format node numbering # # TreeLine, an information storage program # Copyright (C) 2019, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re class NumberingGroup: """Class to store a multi-level numbering format and apply it. """ def __init__(self, numFormat=''): """Initialize a multi-level numbering format. Arguments: numFormat -- a string describing the format """ self.basicFormats = [] self.sectionStyle = False if numFormat: self.setFormat(numFormat) def setFormat(self, numFormat): """Set a new number format. Arguments: numFormat -- a string describing the format """ self.sectionStyle = False formats = _splitText(numFormat.replace('..', '.'), '/') if len(formats) < 2: formats = _splitText(numFormat.replace('//', '/'), '.') if len(formats) > 1: self.sectionStyle = True self.basicFormats = [BasicNumbering(numFormat) for numFormat in formats] def numString(self, inputNumStr): """Return a number string for the given level and input. The current numbering level is the segment length of the input. Raises ValueError on a bad input string. Arguments: inputNumStr -- a dot-separated string of integers """ if not inputNumStr: return '' inputNums = [int(num) for num in inputNumStr.split('.')] if self.sectionStyle: basicFormats = self.basicFormats[:] if len(basicFormats) < len(inputNums): basicFormats.extend([basicFormats[-1]] * (len(inputNums) - len(basicFormats))) results = [basicFormat.numString(num) for basicFormat, num in zip(basicFormats, inputNums)] return '.'.join(results) else: level = len(inputNums) - 1 try: basicFormat = self.basicFormats[level] except IndexError: basicFormat = self.basicFormats[-1] return basicFormat.numString(inputNums[level]) _formatRegEx = re.compile(r'(.*?)([1AaIi]{1,2})(\W*)$') class BasicNumbering: """Class to store an individaul numbering format and apply it. """ def __init__(self, numFormat=''): """Initialize a basic numbering format. Arguments: numFormat -- a string describing the format """ self.numFunction = _stringFromNum self.upperCase = True self.prefix = '' self.suffix = '' if numFormat: self.setFormat(numFormat) def setFormat(self, numFormat): """Set a new number format. Arguments: numFormat -- a string describing the format """ match = _formatRegEx.match(numFormat) if match: self.prefix, series, self.suffix = match.groups() if series == '1': self.numFunction = _stringFromNum elif series in 'Aa': self.numFunction = _alphaFromNum elif series in 'AAaa': self.numFunction = _alpha2FromNum else: self.numFunction = _romanFromNum self.upperCase = series.isupper() else: self.prefix = numFormat self.numFunction = _stringFromNum def numString(self, num): """Return a number string for the given integer. Arguments: num -- the integer to convert """ return '{0}{1}{2}'.format(self.prefix, self.numFunction(num, self.upperCase), self.suffix) def _stringFromNum(num, case=None): """Return a number string from an integer. Arguments: num -- the integer to convert case -- an unused placeholder """ if num > 0: return repr(num) return '' def _alphaFromNum(num, upperCase=True): """Return an alphabetic string from an integer. Sequence is 'A', 'B' ... 'Z', 'AA', 'BB' ... 'ZZ', 'AAA', 'BBB' ... Arguments: num -- the integer to convert upperCase -- return an upper case string if true """ if num <= 0: return '' result = '' charPos = (num - 1) % 26 char = chr(charPos + ord('A')) qty = (num - 1) // 26 + 1 result = char * qty if not upperCase: result = result.lower() return result def _alpha2FromNum(num, upperCase=True): """Return an alphabetic string from an integer. Sequence is 'AA', 'BB' ... 'ZZ', 'AAA', 'BBB' ... 'ZZZ', 'AAAA', 'BBBB' ... Arguments: num -- the integer to convert upperCase -- return an upper case string if true """ if num <= 0: return '' result = '' charPos = (num - 1) % 26 char = chr(charPos + ord('A')) qty = (num - 1) // 26 + 2 result = char * qty if not upperCase: result = result.lower() return result _romanDict = {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 _romanFromNum(num, upperCase=True): """Return a roman numeral string from an integer. Arguments: num -- the integer to convert upperCase -- return an upper case string if true """ if num <= 0 or num >= 4000: return '' result = '' factor = 1000 while num: digit = num - (num % factor) result += _romanDict[digit] factor = factor // 10 num -= digit if not upperCase: result = result.lower() return result def _splitText(textStr, delimitChar): """Split text using the given delimitter and return a list. Double delimitters are not split and empty parts are ignored. Arguments: textStr -- the text to split delimitChar -- the delimitter """ result = [] textStr = textStr.replace(delimitChar * 2, '\0') for text in textStr.split(delimitChar): text = text.replace('\0', delimitChar) if text: result.append(text) return result TreeLine/source/treemodel.py0000644000175000017500000002060613714637332015112 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treemodel.py, provides a class for the tree's data # # TreeLine, an information storage program # Copyright (C) 2017, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import json from PyQt5.QtCore import (QAbstractItemModel, QMimeData, QModelIndex, Qt, pyqtSignal) import undo import treestructure import globalref class TreeModel(QAbstractItemModel): """Class interfacing between the tree structure and the tree view. """ # first arg is set file modified, second is update trees in other views treeModified = pyqtSignal(bool, bool) def __init__(self, treeStructure, parent=None): """Initialize a TreeModel. Arguments: treeStructure -- a ref to the main tree structure parent -- optional QObject parent for the model """ super().__init__(parent) self.treeStructure = treeStructure def index(self, row, column, parentIndex): """Returns the index of a spot in the model based on the parent index. Uses createIndex() to generate the model indices. Arguments: row -- the row of the model node column -- the column (always 0 for now) parentIndex -- the parent's model index in the tree structure """ try: if not parentIndex.isValid(): node = self.treeStructure.childList[row] fakeSpot = list(self.treeStructure.spotRefs)[0] spot = node.matchedSpot(fakeSpot) return self.createIndex(row, column, spot) parentSpot = parentIndex.internalPointer() node = parentSpot.nodeRef.childList[row] return self.createIndex(row, column, node.matchedSpot(parentSpot)) except IndexError: return QModelIndex() def parent(self, index): """Returns the parent model index of the spot at the given index. Arguments: index -- the child model index """ parentSpot = index.internalPointer().parentSpot if parentSpot.parentSpot: return self.createIndex(parentSpot.row(), 0, parentSpot) return QModelIndex() def rowCount(self, parentIndex): """Returns the number of children for the spot at the given index. Arguments: parentIndex -- the parent model index """ try: parentSpot = parentIndex.internalPointer() return parentSpot.nodeRef.numChildren() except AttributeError: # top level if no parentIndex return len(self.treeStructure.childList) def columnCount(self, parentIndex): """The number of columns -- always 1 for now. """ return 1 def data(self, index, role=Qt.DisplayRole): """Return the output data for the node in the given role. Arguments: index -- the spot's model index role -- the type of data requested """ spot = index.internalPointer() if not spot: return None node = spot.nodeRef if role in (Qt.DisplayRole, Qt.EditRole): return node.title() if (role == Qt.DecorationRole and globalref.genOptions['ShowTreeIcons']): return globalref.treeIcons.getIcon(node.formatRef.iconName, True) return None def setData(self, index, value, role=Qt.EditRole): """Set node title after edit operation. Return True on success. Arguments: index -- the node's model index value -- the string result of the editing role -- the edit role of the data """ if role != Qt.EditRole: return super().setData(index, value, role) node = index.internalPointer().nodeRef dataUndo = undo.DataUndo(self.treeStructure.undoList, node) if node.setTitle(value): self.dataChanged.emit(index, index) self.treeModified.emit(True, False) return True self.treeStructure.undoList.removeLastUndo(dataUndo) return False def flags(self, index): """Return the flags for the node at the given index. Arguments: index -- the node's model index """ return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled) def mimeData(self, indexList): """Return a mime data object for the given node index branches. Arguments: indexList -- a list of node indexes to convert """ spots = [index.internalPointer() for index in indexList] # remove selections from the same branch TreeModel.storedDragSpots = [spot for spot in spots if spot.parentSpotSet(). isdisjoint(set(spots))] nodes = [spot.nodeRef for spot in TreeModel.storedDragSpots] TreeModel.storedDragModel = self struct = treestructure.TreeStructure(topNodes=nodes, addSpots=False) generics = {formatRef.genericType for formatRef in struct.treeFormats.values() if formatRef.genericType} for generic in generics: genericRef = self.treeStructure.treeFormats[generic] struct.treeFormats.addTypeIfMissing(genericRef) for formatRef in genericRef.derivedTypes: struct.treeFormats.addTypeIfMissing(formatRef) data = struct.fileData() dataStr = json.dumps(data, indent=0, sort_keys=True) mime = QMimeData() mime.setData('application/json', bytes(dataStr, encoding='utf-8')) return mime def mimeTypes(self): """Return a list of supported mime types for model objects. """ return ['application/json'] def supportedDropActions(self): """Return drop action enum values that are supported by this model. """ return Qt.CopyAction | Qt.MoveAction def dropMimeData(self, mimeData, dropAction, row, column, index): """Decode mime data and add as a child node to the given index. Return True if successful. Arguments: mimeData -- data for the node branch to be added dropAction -- a drop type enum value row -- a row number for the drop location column -- the column number for the drop location (normally 0) index -- the index of the parent node for the drop """ parent = (index.internalPointer().nodeRef if index.internalPointer() else self.treeStructure) isMove = (dropAction == Qt.MoveAction and TreeModel.storedDragModel == self) undoParents = [parent] if isMove: moveParents = {spot.parentSpot.nodeRef for spot in TreeModel.storedDragSpots} undoParents.extend(list(moveParents)) newStruct = treestructure.structFromMimeData(mimeData) # check for valid structure and no circular clone ref and not siblings: if newStruct and (not isMove or (not parent.uId in newStruct.nodeDict and (row >= 0 or {node.uId for node in parent.childList}. isdisjoint({node.uId for node in newStruct.childList})))): undo.ChildListUndo(self.treeStructure.undoList, undoParents, treeFormats=self.treeStructure.treeFormats) if isMove: for spot in TreeModel.storedDragSpots: self.treeStructure.deleteNodeSpot(spot) newStruct.replaceClonedBranches(self.treeStructure) else: newStruct.replaceDuplicateIds(self.treeStructure.nodeDict) self.treeStructure.addNodesFromStruct(newStruct, parent, row) return True return False TreeLine/source/treeline.py0000755000175000017500000001257413760244755014755 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treeline.py, the main program file # # TreeLine, an information storage program # Copyright (C) 2019, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** __progname__ = 'TreeLine' __version__ = '3.1.4' __author__ = 'Doug Bell' docPath = None # modified by install script if required iconPath = None # modified by install script if required templatePath = None # modified by install script if required samplePath = None # modified by install script if required translationPath = 'translations' import sys import pathlib import os.path import argparse import locale import builtins from PyQt5.QtCore import QCoreApplication, QTranslator from PyQt5.QtWidgets import QApplication, qApp def loadTranslator(fileName, app): """Load and install qt translator, return True if sucessful. Arguments: fileName -- the translator file to load app -- the main QApplication """ translator = QTranslator(app) # use abspath() - pathlib's resolve() can be buggy with network drives modPath = pathlib.Path(os.path.abspath(sys.path[0])) if modPath.is_file(): modPath = modPath.parent # for frozen binary path = modPath / translationPath result = translator.load(fileName, str(path)) if not result: path = modPath.parent / translationPath result = translator.load(fileName, str(path)) if not result: path = modPath.parent / 'i18n' / translationPath result = translator.load(fileName, str(path)) if result: QCoreApplication.installTranslator(translator) return True else: print('Warning: translation file "{0}" could not be loaded'. format(fileName)) return False def setupTranslator(app, lang=''): """Set language, load translators and setup translator functions. Return the language setting Arguments: app -- the main QApplication lang -- language setting from the command line """ try: locale.setlocale(locale.LC_ALL, '') except locale.Error: pass if not lang: lang = os.environ.get('LC_MESSAGES', '') if not lang: lang = os.environ.get('LANG', '') if not lang: try: lang = locale.getdefaultlocale()[0] except ValueError: pass if not lang: lang = '' numTranslators = 0 if lang and lang[:2] not in ['C', 'en']: numTranslators += loadTranslator('qt_{0}'.format(lang), app) numTranslators += loadTranslator('treeline_{0}'.format(lang), app) def translate(text, comment=''): """Translation function, sets context to calling module's filename. Arguments: text -- the text to be translated comment -- a comment used only as a guide for translators """ try: frame = sys._getframe(1) fileName = frame.f_code.co_filename finally: del frame context = pathlib.Path(fileName).stem return QCoreApplication.translate(context, text, comment) def markNoTranslate(text, comment=''): """Dummy translation function, only used to mark text. Arguments: text -- the text to be translated comment -- a comment used only as a guide for translators """ return text if numTranslators: builtins._ = translate else: builtins._ = markNoTranslate builtins.N_ = markNoTranslate return lang exceptDialog = None def handleException(excType, value, tb): """Handle uncaught exceptions, show debug info to the user. Called from sys.excepthook. Arguments: excType -- execption class value -- execption error text tb -- the traceback object """ import miscdialogs global exceptDialog exceptDialog = miscdialogs.ExceptionDialog(excType, value, tb) exceptDialog.show() if not QApplication.activeWindow(): qApp.exec_() # start event loop in case it's not running yet if __name__ == '__main__': """Main event loop for TreeLine """ app = QApplication(sys.argv) parser = argparse.ArgumentParser() parser.add_argument('--lang', help='language code for GUI translation') parser.add_argument('fileList', nargs='*', metavar='filename', help='input filename(s) to load') args = parser.parse_args() # use abspath() - pathlib's resolve() can be buggy with network drives pathObjects = [pathlib.Path(os.path.abspath(path)) for path in args.fileList] # must setup translator before any treeline module imports lang = setupTranslator(app, args.lang) import globalref globalref.localTextEncoding = locale.getpreferredencoding() sys.excepthook = handleException import treemaincontrol treeMainControl = treemaincontrol.TreeMainControl(pathObjects) app.exec_() TreeLine/source/matheval.py0000644000175000017500000005204513734503545014735 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # matheval.py, provides a safe eval of mathematical expressions # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import ast import enum import datetime import builtins import fieldformat import gennumber from math import * _nowDateString = 'Now_Date' _nowTimeString = 'Now_Time' _nowDateTimeString = 'Now_Date_Time' def sum(*args): """Override the builtin sum function to handle multiple arguments. Arguments: *args -- lists of numbers or individual numbers """ fullList = [] for arg in args: if hasattr(arg, 'extend'): fullList.extend(arg) else: fullList.append(arg) return builtins.sum(fullList) def max(*args): """Override the builtin max function to expand list arguments. Arguments: *args -- lists of numbers or individual numbers """ fullList = [] for arg in args: if hasattr(arg, 'extend'): fullList.extend(arg) else: fullList.append(arg) if not fullList: return 0 return builtins.max(fullList) def min(*args): """Override the builtin min function to expand list arguments. Arguments: *args -- lists of numbers or individual numbers """ fullList = [] for arg in args: if hasattr(arg, 'extend'): fullList.extend(arg) else: fullList.append(arg) if not fullList: return 0 return builtins.min(fullList) def mean(*args): """Added function to calculate the arithmetic average. Arguments: *args -- lists of numbers or individual numbers """ fullList = [] for arg in args: if hasattr(arg, 'extend'): fullList.extend(arg) else: fullList.append(arg) if not fullList: return 0 return builtins.sum(fullList) / len(fullList) # don't use pow() function from math library pow = builtins.pow def startswith(text, firstText): """Added compare function, returns true if text starts with firstText. Arguments: text -- the string to check firstText -- the starting text """ return str(text).startswith(str(firstText)) def endswith(text, firstText): """Added compare function, returns true if text ends with firstText. Arguments: text -- the string to check firstText -- the ending text """ return str(text).endswith(str(firstText)) def contains(text, innerText): """Added compare function, returns true if text contains innerText. Arguments: text -- the string to check innerText -- the inside text """ return str(innerText) in str(text) def join(sep, *args): """Added text function to combine strings. Arguments: sep -- the separator string *args -- lists of strings or individual strings to combine """ fullList = [] for arg in args: if hasattr(arg, 'extend'): fullList.extend(arg) else: fullList.append(arg) return sep.join([str(i) for i in fullList if str(i)]) def upper(text): """Added text function for upper case. Arguments: text -- the string to modify """ return str(text).upper() def lower(text): """Added text function for lower case. Arguments: text -- the string to modify """ return str(text).lower() def replace(text, oldText, newText): """Added text function to replace strings. Arguments: text -- the string to modify oldText -- the string to be replaced newText -- the replacement string """ return str(text).replace(str(oldText), str(newText)) _fieldSplitRe = re.compile(r'{\*(\*|\$|&|#|\b)([\w_\-.]+)\*}') class MathEquation: """Class to parse, check, store and evaluate a Math field equation. """ def __init__(self, eqnText=''): """Initialize the MathEquation. Arguments: eqnText -- the text of an equation to be parsed """ self.fieldRefs = [] self.formattedEqnText = '' self.parseEquation(eqnText) def equationText(self): """Return the text representation of the equation. """ fieldNames = ['{{*{0}{1}*}}'.format(ref.tagPrefix, ref.fieldName) for ref in self.fieldRefs] return self.formattedEqnText.format(*fieldNames) def validate(self): """Check if the equation is valid (or empty). Use ones as fake input and use ast to verify legality. Raises a ValueError if the equation is not valid. """ if not self.formattedEqnText: return inputs = [ref.testValue for ref in self.fieldRefs] checker = SafeEvalChecker() try: eqn = self.formattedEqnText.format(*inputs) except IndexError: raise ValueError(_('Illegal "{}" characters')) checker.check(eqn) try: result = eval(eqn) if isinstance(result, list): raise TypeError('list result not allowed') except NameError as err: raise ValueError(err) except TypeError as err: if 'list' in str(err) and '&' in [ref.tagPrefix for ref in self.fieldRefs]: msg = _('Child references must be combined in a function') raise ValueError(msg) except ZeroDivisionError: pass def equationValue(self, eqnNode, resultType, zeroValue=0, noMarkup=True): """Return a value for the equation in the given node. Return None if references are invalid. Raise a ValueError for illegal math operations. Arguments: eqnNode -- the node containing the equation to evaluate resultType -- the result type from fieldformat zeroValue -- the value to use for blanks noMarkup -- if true, remove html markup """ zeroBlanks = eqnNode.treeStructureRef().mathZeroBlanks checker = SafeEvalChecker() inputs = [] for ref in self.fieldRefs: inp = ref.referenceValue(eqnNode, zeroBlanks, zeroValue, noMarkup) if inp == None and not zeroBlanks: return None if (resultType == fieldformat.MathResult.number and hasattr(inp, 'format')): try: checker.check(inp) inp = eval(inp) except Exception: inp = repr(inp) else: inp = repr(inp) inputs.append(inp) eqn = self.formattedEqnText.format(*inputs) try: return eval(eqn) except Exception as err: raise ValueError(err) def parseEquation(self, eqnText): """Replace the stored equation by parsing the given text. Creates formatted equation text and a list of field references. Arguments: eqnText -- the text of an equation to be parsed """ self.fieldRefs = [] self.formattedEqnText = _fieldSplitRe.sub(self._replFunc, eqnText) def _replFunc(self, matchObj): """Adds a field ref for each field match from the parser. Returns a string format placeholder as the replacement text. Arguments: matchObj -- the field match object """ fieldRefType = matchObj.group(1) fieldRefName = matchObj.group(2) fieldRefSelector = {'': EquationFieldRef, '*': EquationParentRef, '$': EquationRootRef, '&': EquationChildRef, '#': EquationChildCountRef} fieldRef = fieldRefSelector[fieldRefType](fieldRefName) self.fieldRefs.append(fieldRef) return '{}' # recursive equation ref eval directions EvalDir = enum.IntEnum('EvalDir', 'downward upward optional') class EquationFieldRef: """Class to store and eval individual field references in a Math equation. This base class handles references within the same node. """ tagPrefix = '' testValue = 1 evalDirection = EvalDir.optional def __init__(self, fieldName): """Initialize the field references. Arguments: fieldName -- the name of the referenced field """ self.fieldName = fieldName self.eqnNodeTypeName = '' self.eqnFieldName = '' def referenceValue(self, eqnNode, zeroBlanks=True, zeroValue=0, noMarkup=True): """Return the value of the field referenced in a given node. Return None if blank or doesn't exist and not zeroBlanks, raise a ValueError if it isn't a number. Arguments: eqnNode -- the node containing the equation to evaluate zeroBlanks -- replace blank fields with zeroValue if True zeroValue -- the value to use for blanks noMarkup -- if true, remove html markup """ try: return (eqnNode.formatRef.fieldDict[self.fieldName]. mathValue(eqnNode, zeroBlanks, noMarkup)) except KeyError: if self.fieldName == _nowDateString: return (datetime.date.today() - fieldformat.DateField.refDate).days elif self.fieldName == _nowTimeString: now = datetime.datetime.combine(fieldformat.DateField.refDate, datetime.datetime.now().time()) ref = datetime.datetime.combine(fieldformat.DateField.refDate, fieldformat.TimeField.refTime) return (now - ref).seconds elif self.fieldName == _nowDateTimeString: return (datetime.datetime.now() - fieldformat.DateTimeField.refDateTime).total_seconds() return zeroValue if zeroBlanks else None def dependentEqnNodes(self, refNode): """Return a list of equation node(s) that reference the given node. Arguments: refNode -- the node containing the referenced field """ if refNode.formatRef.name == self.eqnNodeTypeName: return [refNode] return [] class EquationParentRef(EquationFieldRef): """Class to store and eval parent field references in a Math equation. """ tagPrefix = '*' testValue = 1 evalDirection = EvalDir.downward def referenceValue(self, eqnNode, zeroBlanks=True, zeroValue=0, noMarkup=True): """Return the parent field value referenced from a given node. Return None if blank or doesn't exist and not zeroBlanks, raise a ValueError if it isn't a number. Arguments: eqnNode -- the node containing the equation to evaluate zeroBlanks -- replace blank fields with zeroValue if True zeroValue -- the value to use for blanks noMarkup -- if true, remove html markup """ node = eqnNode.spotByNumber(0).parentSpot.nodeRef if not node.formatRef: return zeroValue if zeroBlanks else None try: return (node.formatRef.fieldDict[self.fieldName]. mathValue(node, zeroBlanks, noMarkup)) except KeyError: return zeroValue if zeroBlanks else None def dependentEqnNodes(self, refNode): """Return a list of equation node(s) that reference the given node. Arguments: refNode -- the node containing the referenced field """ return [node for node in refNode.childList if node.formatRef.name == self.eqnNodeTypeName] class EquationRootRef(EquationFieldRef): """Class to store and eval root node field references in a Math equation. """ tagPrefix = '$' testValue = 1 evalDirection = EvalDir.downward def referenceValue(self, eqnNode, zeroBlanks=True, zeroValue=0, noMarkup=True): """Return the root field value referenced from a given node. Return None if blank or doesn't exist and not zeroBlanks, raise a ValueError if it isn't a number. Arguments: eqnNode -- the node containing the equation to evaluate zeroBlanks -- replace blank fields with zeroValue if True zeroValue -- the value to use for blanks noMarkup -- if true, remove html markup """ node = eqnNode.spotByNumber(0).spotChain()[0].nodeRef try: return (node.formatRef.fieldDict[self.fieldName]. mathValue(node, zeroBlanks, noMarkup)) except KeyError: return zeroValue if zeroBlanks else None def dependentEqnNodes(self, refNode): """Return a list of equation node(s) that reference the given node. Arguments: refNode -- the node containing the referenced field """ if 1 not in {len(spot.spotChain()) for spot in refNode.spotRefs}: # not a root node return [] refs = [node for node in refNode.descendantGen() if node.formatRef.name == self.eqnNodeTypeName] if refs and refs[0] is refNode: refs = refs[1:] return refs class EquationChildRef(EquationFieldRef): """Class to store and eval child field references in a Math equation. """ tagPrefix = '&' testValue = [1] evalDirection = EvalDir.upward def referenceValue(self, eqnNode, zeroBlanks=True, zeroValue=0, noMarkup=True): """Return a list with child field values referenced from a given node. Return None if there are blanks and zeroBlanks is false, raise a ValueError if any aren't a number. Arguments: eqnNode -- the node containing the equation to evaluate zeroBlanks -- replace blank fields with zeroValue if True zeroValue -- the value to use for blanks """ result = [] for node in eqnNode.childList: try: num = (node.formatRef.fieldDict[self.fieldName]. mathValue(node, zeroBlanks, noMarkup)) if num == None: return None result.append(num) except KeyError: if not zeroBlanks: return None if not result: result = [zeroValue] return result def dependentEqnNodes(self, refNode): """Return a list of equation node(s) that reference the given node. Arguments: refNode -- the node containing the referenced field """ node = refNode.spotByNumber(0).parentSpot.nodeRef if node.formatRef and node.formatRef.name == self.eqnNodeTypeName: return [node] return [] class EquationChildCountRef(EquationFieldRef): """Class to store and eval child count references in a Math equation. """ tagPrefix = '#' testValue = 1 evalDirection = EvalDir.optional def referenceValue(self, eqnNode, zeroBlanks=True, zeroValue=0, noMarkup=True): """Return the child count referenced from the given node. Arguments: eqnNode -- the node containing the equation to evaluate zeroBlanks -- replace blank fields with zeroValue if True zeroValue -- the value to use for blanks noMarkup -- if true, remove html markup """ return len(eqnNode.childList) def dependentEqnNodes(self, refNode): """Return a list of equation node(s) that reference the given node. Arguments: refNode -- the node containing the referenced field """ node = refNode.spotByNumber(0).parentSpot.nodeRef if node and node.formatRef.name == self.eqnNodeTypeName: return [node] return [] class RecursiveEqnRef: """Class to store a references to other equations in a tree structure. Resolves sequence and direction of global evaluations. """ recursiveRefDict = {} def __init__(self, eqnTypeName, eqnField): """Initialize the RecursiveEquationRef. Arguments: eqnTypeName -- the type format name contining the equation field eqnField -- the field with the equation to eval for other eqn refs """ self.eqnTypeName = eqnTypeName self.eqnField = eqnField self.evalSequence = 0 self.evalDirection = EvalDir.optional def setPriorities(self, visitedFields=None): """Recursively set sequence and direction for evaluation. Arguments: visitedFields -- set of used eqn field names to check circular refs """ if self.evalSequence != 0: return if visitedFields == None: visitedFields = set() visitedFields = visitedFields.copy() visitedFields.add(self.eqnField.name) self.evalSequence = 1 for fieldRef in self.eqnField.equation.fieldRefs: if (fieldRef.fieldName in visitedFields and fieldRef.tagPrefix != '#' and (self.eqnField.name != fieldRef.fieldName or fieldRef.evalDirection == EvalDir.optional)): raise CircularMathError() for eqnRef in self.recursiveRefDict.get(fieldRef.fieldName, []): eqnRef.setPriorities(visitedFields) if eqnRef.evalSequence >= self.evalSequence: self.evalDirection = fieldRef.evalDirection self.evalSequence = eqnRef.evalSequence if (self.evalDirection != eqnRef.evalDirection or self.evalDirection == EvalDir.optional): self.evalSequence += 1 def __lt__(self, other): """Use sequence and direction as comparison keys for sorting. Arguments: other -- the equation ref to compare """ return ((self.evalSequence, self.evalDirection) < (other.evalSequence, other.evalDirection)) class CircularMathError(Exception): """Exception raised when circular references are found in math fields. """ pass allowedFunctions = set(['abs', 'float', 'int', 'len', 'max', 'min', 'pow', 'round', 'sum', 'mean', 'ceil', 'fabs', 'factorial', 'floor', 'fmod', 'fsum', 'trunc', 'exp', 'log', 'log10', 'pow', 'sqrt', 'acos', 'asin', 'atan', 'cos', 'sin', 'tan', 'hypot', 'degrees', 'radians', 'pi', 'e', 'startswith', 'endswith', 'contains', 'join', 'upper', 'lower', 'replace']) allowedNodeTypes = set(['Module', 'Expr', 'Name', 'NameConstant', 'Constant', 'Load', 'IfExp', 'Compare', 'Num', 'Str', 'Tuple', 'List', 'BinOp', 'UnaryOp', 'Add', 'Sub', 'Mult', 'Div', 'Mod', 'Pow', 'FloorDiv', 'Invert', 'Not', 'UAdd', 'USub', 'Eq', 'NotEq', 'Lt', 'LtE', 'Gt', 'GtE', 'Is', 'IsNot', 'In', 'NotIn', 'BoolOp', 'And', 'Or']) class SafeEvalChecker(ast.NodeVisitor): """Class to check that only safe functions are used in an eval expression. Raises a ValueError if unsafe or non-numeric operations are present. Ref. stackoverflow.com questions 10661079 and 12523516 """ def check(self, expr): """Check the given expression for non-numeric operations. Arguments: expr -- the expression string to check """ try: tree = ast.parse(expr) except SyntaxError: raise ValueError(_('Illegal syntax in equation')) self.visit(tree) def visit_Call(self, node): """Check for allowed functions only. Arguments: node -- the ast node being checked """ if node.func.id in allowedFunctions: super().generic_visit(node) else: raise ValueError(_('Illegal function present: {0}'). format(node.func.id)) def generic_visit(self, node): """Check for allowed node types and operators. Arguments: node -- the ast node being checked """ if type(node).__name__ in allowedNodeTypes: super().generic_visit(node) else: raise ValueError(_('Illegal object type or operator: {0}'). format(type(node).__name__)) if __name__ == '__main__': checker = SafeEvalChecker() try: print('Enter expression: ') expr = input() checker.check(expr) except ValueError as err: print(err) else: print(eval(expr)) TreeLine/source/miscdialogs.py0000644000175000017500000023057713626535361015443 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # miscdialogs.py, provides classes for various control dialogs # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import enum import re import sys import operator import collections import datetime import platform import traceback from PyQt5.QtCore import Qt, pyqtSignal, PYQT_VERSION_STR, qVersion from PyQt5.QtGui import QFont, QKeySequence, QTextDocument, QTextOption from PyQt5.QtWidgets import (QAbstractItemView, QApplication, QButtonGroup, QCheckBox, QComboBox, QDialog, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox, QPlainTextEdit, QPushButton, QRadioButton, QScrollArea, QSpinBox, QTabWidget, QTextEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget) import options import printdialogs import undo import globalref try: from __main__ import __version__ except ImportError: __version__ = '' class RadioChoiceDialog(QDialog): """Dialog for choosing between a list of text items (radio buttons). Dialog title, group heading, button text and return text can be set. """ def __init__(self, title, heading, choiceList, parent=None): """Create the radio choice dialog. Arguments: title -- the window title heading -- the groupbox text choiceList -- tuples of button text and return values parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(title) topLayout = QVBoxLayout(self) self.setLayout(topLayout) groupBox = QGroupBox(heading) topLayout.addWidget(groupBox) groupLayout = QVBoxLayout(groupBox) self.buttonGroup = QButtonGroup(self) for text, value in choiceList: if value != None: button = QRadioButton(text) button.returnValue = value groupLayout.addWidget(button) self.buttonGroup.addButton(button) else: # add heading if no return value label = QLabel('{0}:'.format(text)) groupLayout.addWidget(label) self.buttonGroup.buttons()[0].setChecked(True) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) groupBox.setFocus() def addLabelBox(self, heading, text): """Add a group box with text above the radio button group. Arguments: heading -- the groupbox text text - the label text """ labelBox = QGroupBox(heading) self.layout().insertWidget(0, labelBox) labelLayout = QVBoxLayout(labelBox) label = QLabel(text) labelLayout.addWidget(label) def selectedButton(self): """Return the value of the selected button. """ return self.buttonGroup.checkedButton().returnValue class FieldSelectDialog(QDialog): """Dialog for selecting a sequence from a list of field names. """ def __init__(self, title, heading, fieldList, parent=None): """Create the field select dialog. Arguments: title -- the window title heading -- the groupbox text fieldList -- the list of field names to select parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(title) self.selectedFields = [] topLayout = QVBoxLayout(self) self.setLayout(topLayout) groupBox = QGroupBox(heading) topLayout.addWidget(groupBox) groupLayout = QVBoxLayout(groupBox) self.listView = QTreeWidget() groupLayout.addWidget(self.listView) self.listView.setHeaderLabels(['#', _('Fields')]) self.listView.setRootIsDecorated(False) self.listView.setSortingEnabled(False) self.listView.setSelectionMode(QAbstractItemView.MultiSelection) for field in fieldList: QTreeWidgetItem(self.listView, ['', field]) self.listView.resizeColumnToContents(0) self.listView.resizeColumnToContents(1) self.listView.itemSelectionChanged.connect(self.updateSelectedFields) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) self.okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.clicked.connect(self.accept) self.okButton.setEnabled(False) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.listView.setFocus() def updateSelectedFields(self): """Update the TreeView and the list of selected fields. """ itemList = [self.listView.topLevelItem(i) for i in range(self.listView.topLevelItemCount())] for item in itemList: if item.isSelected(): if item.text(1) not in self.selectedFields: self.selectedFields.append(item.text(1)) elif item.text(1) in self.selectedFields: self.selectedFields.remove(item.text(1)) for item in itemList: if item.isSelected(): item.setText(0, str(self.selectedFields.index(item.text(1)) + 1)) else: item.setText(0, '') self.okButton.setEnabled(len(self.selectedFields)) class FilePropertiesDialog(QDialog): """Dialog for setting file parameters like compression and encryption. """ def __init__(self, localControl, parent=None): """Create the file properties dialog. Arguments: localControl -- a reference to the file's local control parent -- the parent window """ super().__init__(parent) self.localControl = localControl self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('File Properties')) topLayout = QVBoxLayout(self) self.setLayout(topLayout) groupBox = QGroupBox(_('File Storage')) topLayout.addWidget(groupBox) groupLayout = QVBoxLayout(groupBox) self.compressCheck = QCheckBox(_('&Use file compression')) self.compressCheck.setChecked(localControl.compressed) groupLayout.addWidget(self.compressCheck) self.encryptCheck = QCheckBox(_('Use file &encryption')) self.encryptCheck.setChecked(localControl.encrypted) groupLayout.addWidget(self.encryptCheck) groupBox = QGroupBox(_('Spell Check')) topLayout.addWidget(groupBox) groupLayout = QHBoxLayout(groupBox) label = QLabel(_('Language code or\ndictionary (optional)')) groupLayout.addWidget(label) self.spellCheckEdit = QLineEdit() self.spellCheckEdit.setText(self.localControl.spellCheckLang) groupLayout.addWidget(self.spellCheckEdit) groupBox = QGroupBox(_('Math Fields')) topLayout.addWidget(groupBox) groupLayout = QVBoxLayout(groupBox) self.zeroBlanks = QCheckBox(_('&Treat blank fields as zeros')) self.zeroBlanks.setChecked(localControl.structure.mathZeroBlanks) groupLayout.addWidget(self.zeroBlanks) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) def accept(self): """Store the results. """ if (self.localControl.compressed != self.compressCheck.isChecked() or self.localControl.encrypted != self.encryptCheck.isChecked() or self.localControl.spellCheckLang != self.spellCheckEdit.text() or self.localControl.structure.mathZeroBlanks != self.zeroBlanks.isChecked()): undo.ParamUndo(self.localControl.structure.undoList, [(self.localControl, 'compressed'), (self.localControl, 'encrypted'), (self.localControl, 'spellCheckLang'), (self.localControl.structure, 'mathZeroBlanks')]) self.localControl.compressed = self.compressCheck.isChecked() self.localControl.encrypted = self.encryptCheck.isChecked() self.localControl.spellCheckLang = self.spellCheckEdit.text() self.localControl.structure.mathZeroBlanks = (self.zeroBlanks. isChecked()) super().accept() else: super().reject() class PasswordDialog(QDialog): """Dialog for password entry and optional re-entry. """ remember = True def __init__(self, retype=True, fileLabel='', parent=None): """Create the password dialog. Arguments: retype -- require a 2nd password entry if True fileLabel -- file name to show if given parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Encrypted File Password')) self.password = '' topLayout = QVBoxLayout(self) self.setLayout(topLayout) if fileLabel: prompt = _('Type Password for "{0}":').format(fileLabel) else: prompt = _('Type Password:') self.editors = [self.addEditor(prompt, topLayout)] self.editors[0].setFocus() if retype: self.editors.append(self.addEditor(_('Re-Type Password:'), topLayout)) self.editors[0].returnPressed.connect(self.editors[1].setFocus) self.editors[-1].returnPressed.connect(self.accept) self.rememberCheck = QCheckBox(_('Remember password during this ' 'session')) self.rememberCheck.setChecked(PasswordDialog.remember) topLayout.addWidget(self.rememberCheck) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QPushButton(_('&OK')) okButton.setAutoDefault(False) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) cancelButton.setAutoDefault(False) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) def addEditor(self, labelText, layout): """Add a password editor to this dialog and return it. Arguments: labelText -- the text for the label layout -- the layout to append it """ label = QLabel(labelText) layout.addWidget(label) editor = QLineEdit() editor.setEchoMode(QLineEdit.Password) layout.addWidget(editor) return editor def accept(self): """Check for valid password and store the result. """ self.password = self.editors[0].text() PasswordDialog.remember = self.rememberCheck.isChecked() if not self.password: QMessageBox.warning(self, 'TreeLine', _('Zero-length passwords are not permitted')) elif len(self.editors) > 1 and self.editors[1].text() != self.password: QMessageBox.warning(self, 'TreeLine', _('Re-typed password did not match')) else: super().accept() for editor in self.editors: editor.clear() self.editors[0].setFocus() class TemplateFileItem: """Helper class to store template paths and info. """ nameExp = re.compile(r'(\d+)([a-zA-Z]+?)_(.+)') def __init__(self, pathObj): """Initialize the path. Arguments: pathObj -- the full path object """ self.pathObj = pathObj self.number = sys.maxsize self.name = '' self.displayName = '' self.langCode = '' if pathObj: self.name = pathObj.stem match = TemplateFileItem.nameExp.match(self.name) if match: num, self.langCode, self.name = match.groups() self.number = int(num) self.displayName = self.name.replace('_', ' ') def sortKey(self): """Return a key for sorting the items by number then name. """ return (self.number, self.displayName) def __eq__(self, other): """Comparison to detect equivalent items. Arguments: other -- the TemplateFileItem to compare """ return (self.displayName == other.displayName and self.langCode == other.langCode) def __hash__(self): """Return a hash code for use in sets and dictionaries. """ return hash((self.langCode, self.displayName)) class TemplateFileDialog(QDialog): """Dialog for listing available template files. """ def __init__(self, title, heading, searchPaths, addDefault=True, parent=None): """Create the template dialog. Arguments: title -- the window title heading -- the groupbox text searchPaths -- list of path objects with available templates addDefault -- if True, add a default (no path) entry parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(title) self.templateItems = [] if addDefault: item = TemplateFileItem(None) item.number = -1 item.displayName = _('Default - Single Line Text') self.templateItems.append(item) topLayout = QVBoxLayout(self) self.setLayout(topLayout) groupBox = QGroupBox(heading) topLayout.addWidget(groupBox) boxLayout = QVBoxLayout(groupBox) self.listBox = QListWidget() boxLayout.addWidget(self.listBox) self.listBox.itemDoubleClicked.connect(self.accept) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) self.okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.readTemplates(searchPaths) self.loadListBox() def readTemplates(self, searchPaths): """Read template file paths into the templateItems list. Arguments: searchPaths -- list of path objects with available templates """ templateItems = set() for path in searchPaths: for templatePath in path.glob('*.trln'): templateItem = TemplateFileItem(templatePath) if templateItem not in templateItems: templateItems.add(templateItem) availLang = set([item.langCode for item in templateItems]) if len(availLang) > 1: lang = 'en' if globalref.lang[:2] in availLang: lang = globalref.lang[:2] templateItems = [item for item in templateItems if item.langCode == lang or not item.langCode] self.templateItems.extend(list(templateItems)) self.templateItems.sort(key = operator.methodcaller('sortKey')) def loadListBox(self): """Load the list box with items from the templateItems list. """ self.listBox.clear() self.listBox.addItems([item.displayName for item in self.templateItems]) self.listBox.setCurrentRow(0) self.okButton.setEnabled(self.listBox.count() > 0) def selectedPath(self): """Return the path object from the selected item. """ item = self.templateItems[self.listBox.currentRow()] return item.pathObj def selectedName(self): """Return the displayed name with underscores from the selected item. """ item = self.templateItems[self.listBox.currentRow()] return item.name class ExceptionDialog(QDialog): """Dialog for showing debug info from an unhandled exception. """ def __init__(self, excType, value, tb, parent=None): """Initialize the exception dialog. Arguments: excType -- execption class value -- execption error text tb -- the traceback object """ super().__init__(parent) self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) self.setWindowTitle(_('TreeLine - Serious Error')) topLayout = QVBoxLayout(self) self.setLayout(topLayout) label = QLabel(_('A serious error has occurred. TreeLine could be ' 'in an unstable state.\n' 'Recommend saving any file changes under another ' 'filename and restart TreeLine.\n\n' 'The debugging info shown below can be copied ' 'and emailed to doug101@bellz.org along with\n' 'an explanation of the circumstances.\n')) topLayout.addWidget(label) textBox = QTextEdit() textBox.setReadOnly(True) pyVersion = '.'.join([repr(num) for num in sys.version_info[:3]]) textLines = ['When: {0}\n'.format(datetime.datetime.now(). isoformat(' ')), 'TreeLine Version: {0}\n'.format(__version__), 'Python Version: {0}\n'.format(pyVersion), 'Qt Version: {0}\n'.format(qVersion()), 'PyQt Version: {0}\n'.format(PYQT_VERSION_STR), 'OS: {0}\n'.format(platform.platform()), '\n'] textLines.extend(traceback.format_exception(excType, value, tb)) textBox.setPlainText(''.join(textLines)) topLayout.addWidget(textBox) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) closeButton = QPushButton(_('&Close')) ctrlLayout.addWidget(closeButton) closeButton.clicked.connect(self.close) FindScope = enum.IntEnum('FindScope', 'fullData titlesOnly') FindType = enum.IntEnum('FindType', 'keyWords fullWords fullPhrase regExp') class FindFilterDialog(QDialog): """Dialog for searching for text within tree titles and data. """ dialogShown = pyqtSignal(bool) def __init__(self, isFilterDialog=False, parent=None): """Initialize the find dialog. Arguments: isFilterDialog -- True for filter dialog, False for find dialog parent -- the parent window """ super().__init__(parent) self.isFilterDialog = isFilterDialog self.setAttribute(Qt.WA_QuitOnClose, False) self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) topLayout = QVBoxLayout(self) self.setLayout(topLayout) textBox = QGroupBox(_('&Search Text')) topLayout.addWidget(textBox) textLayout = QVBoxLayout(textBox) self.textEntry = QLineEdit() textLayout.addWidget(self.textEntry) self.textEntry.textEdited.connect(self.updateAvail) horizLayout = QHBoxLayout() topLayout.addLayout(horizLayout) whatBox = QGroupBox(_('What to Search')) horizLayout.addWidget(whatBox) whatLayout = QVBoxLayout(whatBox) self.whatButtons = QButtonGroup(self) button = QRadioButton(_('Full &data')) self.whatButtons.addButton(button, FindScope.fullData) whatLayout.addWidget(button) button = QRadioButton(_('&Titles only')) self.whatButtons.addButton(button, FindScope.titlesOnly) whatLayout.addWidget(button) self.whatButtons.button(FindScope.fullData).setChecked(True) howBox = QGroupBox(_('How to Search')) horizLayout.addWidget(howBox) howLayout = QVBoxLayout(howBox) self.howButtons = QButtonGroup(self) button = QRadioButton(_('&Key words')) self.howButtons.addButton(button, FindType.keyWords) howLayout.addWidget(button) button = QRadioButton(_('Key full &words')) self.howButtons.addButton(button, FindType.fullWords) howLayout.addWidget(button) button = QRadioButton(_('F&ull phrase')) self.howButtons.addButton(button, FindType.fullPhrase) howLayout.addWidget(button) button = QRadioButton(_('&Regular expression')) self.howButtons.addButton(button, FindType.regExp) howLayout.addWidget(button) self.howButtons.button(FindType.keyWords).setChecked(True) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) if not self.isFilterDialog: self.setWindowTitle(_('Find')) self.previousButton = QPushButton(_('Find &Previous')) ctrlLayout.addWidget(self.previousButton) self.previousButton.clicked.connect(self.findPrevious) self.nextButton = QPushButton(_('Find &Next')) self.nextButton.setDefault(True) ctrlLayout.addWidget(self.nextButton) self.nextButton.clicked.connect(self.findNext) self.resultLabel = QLabel() topLayout.addWidget(self.resultLabel) else: self.setWindowTitle(_('Filter')) self.filterButton = QPushButton(_('&Filter')) ctrlLayout.addWidget(self.filterButton) self.filterButton.clicked.connect(self.startFilter) self.endFilterButton = QPushButton(_('&End Filter')) ctrlLayout.addWidget(self.endFilterButton) self.endFilterButton.clicked.connect(self.endFilter) closeButton = QPushButton(_('&Close')) ctrlLayout.addWidget(closeButton) closeButton.clicked.connect(self.close) self.updateAvail('') def selectAllText(self): """Select all line edit text to prepare for a new entry. """ self.textEntry.selectAll() self.textEntry.setFocus() def updateAvail(self, text='', fileChange=False): """Make find buttons available if search text exists. Arguments: text -- placeholder for signal text (not used) fileChange -- True if window changed while dialog open """ hasEntry = len(self.textEntry.text().strip()) > 0 if not self.isFilterDialog: self.previousButton.setEnabled(hasEntry) self.nextButton.setEnabled(hasEntry) self.resultLabel.setText('') else: window = globalref.mainControl.activeControl.activeWindow if fileChange and window.treeFilterView: filterView = window.treeFilterView self.textEntry.setText(filterView.filterStr) self.whatButtons.button(filterView.filterWhat).setChecked(True) self.howButtons.button(filterView.filterHow).setChecked(True) self.filterButton.setEnabled(hasEntry) self.endFilterButton.setEnabled(window.treeFilterView != None) def find(self, forward=True): """Find another match in the indicated direction. Arguments: forward -- next if True, previous if False """ self.resultLabel.setText('') text = self.textEntry.text() titlesOnly = self.whatButtons.checkedId() == (FindScope.titlesOnly) control = globalref.mainControl.activeControl if self.howButtons.checkedId() == FindType.regExp: try: regExp = re.compile(text) except re.error: QMessageBox.warning(self, 'TreeLine', _('Error - invalid regular expression')) return result = control.findNodesByRegExp([regExp], titlesOnly, forward) elif self.howButtons.checkedId() == FindType.fullWords: regExpList = [] for word in text.lower().split(): regExpList.append(re.compile(r'(?i)\b{}\b'. format(re.escape(word)))) result = control.findNodesByRegExp(regExpList, titlesOnly, forward) elif self.howButtons.checkedId() == FindType.keyWords: wordList = text.lower().split() result = control.findNodesByWords(wordList, titlesOnly, forward) else: # full phrase wordList = [text.lower().strip()] result = control.findNodesByWords(wordList, titlesOnly, forward) if not result: self.resultLabel.setText(_('Search string "{0}" not found'). format(text)) def findPrevious(self): """Find the previous match. """ self.find(False) def findNext(self): """Find the next match. """ self.find(True) def startFilter(self): """Start filtering nodes. """ if self.howButtons.checkedId() == FindType.regExp: try: re.compile(self.textEntry.text()) except re.error: QMessageBox.warning(self, 'TreeLine', _('Error - invalid regular expression')) return filterView = (globalref.mainControl.activeControl.activeWindow. filterView()) filterView.filterWhat = self.whatButtons.checkedId() filterView.filterHow = self.howButtons.checkedId() filterView.filterStr = self.textEntry.text() filterView.updateContents() self.updateAvail() def endFilter(self): """Stop filtering nodes. """ globalref.mainControl.activeControl.activeWindow.removeFilterView() self.updateAvail() def closeEvent(self, event): """Signal that the dialog is closing. Arguments: event -- the close event """ self.dialogShown.emit(False) FindReplaceType = enum.IntEnum('FindReplaceType', 'anyMatch fullWord regExp') class FindReplaceDialog(QDialog): """Dialog for finding and replacing text in the node data. """ dialogShown = pyqtSignal(bool) def __init__(self, parent=None): """Initialize the find and replace dialog. Arguments: parent -- the parent window """ super().__init__(parent) self.setAttribute(Qt.WA_QuitOnClose, False) self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) self.setWindowTitle(_('Find and Replace')) self.matchedSpot = None topLayout = QGridLayout(self) self.setLayout(topLayout) textBox = QGroupBox(_('&Search Text')) topLayout.addWidget(textBox, 0, 0) textLayout = QVBoxLayout(textBox) self.textEntry = QLineEdit() textLayout.addWidget(self.textEntry) self.textEntry.textEdited.connect(self.clearMatch) replaceBox = QGroupBox(_('Replacement &Text')) topLayout.addWidget(replaceBox, 0, 1) replaceLayout = QVBoxLayout(replaceBox) self.replaceEntry = QLineEdit() replaceLayout.addWidget(self.replaceEntry) howBox = QGroupBox(_('How to Search')) topLayout.addWidget(howBox, 1, 0, 2, 1) howLayout = QVBoxLayout(howBox) self.howButtons = QButtonGroup(self) button = QRadioButton(_('Any &match')) self.howButtons.addButton(button, FindReplaceType.anyMatch) howLayout.addWidget(button) button = QRadioButton(_('Full &words')) self.howButtons.addButton(button, FindReplaceType.fullWord) howLayout.addWidget(button) button = QRadioButton(_('Re&gular expression')) self.howButtons.addButton(button, FindReplaceType.regExp) howLayout.addWidget(button) self.howButtons.button(FindReplaceType.anyMatch).setChecked(True) self.howButtons.buttonClicked.connect(self.clearMatch) typeBox = QGroupBox(_('&Node Type')) topLayout.addWidget(typeBox, 1, 1) typeLayout = QVBoxLayout(typeBox) self.typeCombo = QComboBox() typeLayout.addWidget(self.typeCombo) self.typeCombo.currentIndexChanged.connect(self.loadFieldNames) fieldBox = QGroupBox(_('N&ode Fields')) topLayout.addWidget(fieldBox, 2, 1) fieldLayout = QVBoxLayout(fieldBox) self.fieldCombo = QComboBox() fieldLayout.addWidget(self.fieldCombo) self.fieldCombo.currentIndexChanged.connect(self.clearMatch) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout, 3, 0, 1, 2) self.previousButton = QPushButton(_('Find &Previous')) ctrlLayout.addWidget(self.previousButton) self.previousButton.clicked.connect(self.findPrevious) self.nextButton = QPushButton(_('&Find Next')) self.nextButton.setDefault(True) ctrlLayout.addWidget(self.nextButton) self.nextButton.clicked.connect(self.findNext) self.replaceButton = QPushButton(_('&Replace')) ctrlLayout.addWidget(self.replaceButton) self.replaceButton.clicked.connect(self.replace) self.replaceAllButton = QPushButton(_('Replace &All')) ctrlLayout.addWidget(self.replaceAllButton) self.replaceAllButton.clicked.connect(self.replaceAll) closeButton = QPushButton(_('&Close')) ctrlLayout.addWidget(closeButton) closeButton.clicked.connect(self.close) self.resultLabel = QLabel() topLayout.addWidget(self.resultLabel, 4, 0, 1, 2) self.loadTypeNames() self.updateAvail() def updateAvail(self): """Set find & replace buttons available if search text & matches exist. """ hasEntry = (len(self.textEntry.text().strip()) > 0 or self.howButtons.checkedId() == FindReplaceType.anyMatch) self.previousButton.setEnabled(hasEntry) self.nextButton.setEnabled(hasEntry) match = bool(self.matchedSpot and self.matchedSpot is globalref.mainControl.activeControl. currentSelectionModel().currentSpot()) self.replaceButton.setEnabled(match) self.replaceAllButton.setEnabled(match) self.resultLabel.setText('') def clearMatch(self): """Remove reference to matched node if search criteria changes. """ self.matchedSpot = None globalref.mainControl.activeControl.findReplaceSpotRef = (None, 0) self.updateAvail() def loadTypeNames(self): """Load format type names into combo box. """ origTypeName = self.typeCombo.currentText() nodeFormats = globalref.mainControl.activeControl.structure.treeFormats self.typeCombo.blockSignals(True) self.typeCombo.clear() typeNames = nodeFormats.typeNames() self.typeCombo.addItems([_('[All Types]')] + typeNames) origPos = self.typeCombo.findText(origTypeName) if origPos >= 0: self.typeCombo.setCurrentIndex(origPos) self.typeCombo.blockSignals(False) self.loadFieldNames() def loadFieldNames(self): """Load field names into combo box. """ origFieldName = self.fieldCombo.currentText() nodeFormats = globalref.mainControl.activeControl.structure.treeFormats typeName = self.typeCombo.currentText() fieldNames = [] if typeName.startswith('['): for typeName in nodeFormats.typeNames(): for fieldName in nodeFormats[typeName].fieldNames(): if fieldName not in fieldNames: fieldNames.append(fieldName) else: fieldNames.extend(nodeFormats[typeName].fieldNames()) self.fieldCombo.clear() self.fieldCombo.addItems([_('[All Fields]')] + fieldNames) origPos = self.fieldCombo.findText(origFieldName) if origPos >= 0: self.fieldCombo.setCurrentIndex(origPos) self.matchedSpot = None self.updateAvail() def findParameters(self): """Create search parameters based on the dialog settings. Return a tuple of searchText, regExpObj, typeName, and fieldName. """ text = self.textEntry.text() searchText = '' regExpObj = None if self.howButtons.checkedId() == FindReplaceType.anyMatch: searchText = text.lower().strip() elif self.howButtons.checkedId() == FindReplaceType.fullWord: regExpObj = re.compile(r'(?i)\b{}\b'.format(re.escape(text))) else: regExpObj = re.compile(text) typeName = self.typeCombo.currentText() if typeName.startswith('['): typeName = '' fieldName = self.fieldCombo.currentText() if fieldName.startswith('['): fieldName = '' return (searchText, regExpObj, typeName, fieldName) def find(self, forward=True): """Find another match in the indicated direction. Arguments: forward -- next if True, previous if False """ self.matchedSpot = None try: searchText, regExpObj, typeName, fieldName = self.findParameters() except re.error: QMessageBox.warning(self, 'TreeLine', _('Error - invalid regular expression')) self.updateAvail() return control = globalref.mainControl.activeControl if control.findNodesForReplace(searchText, regExpObj, typeName, fieldName, forward): self.matchedSpot = control.currentSelectionModel().currentSpot() self.updateAvail() else: self.updateAvail() self.resultLabel.setText(_('Search text "{0}" not found'). format(self.textEntry.text())) def findPrevious(self): """Find the previous match. """ self.find(False) def findNext(self): """Find the next match. """ self.find(True) def replace(self): """Replace the currently found text. """ searchText, regExpObj, typeName, fieldName = self.findParameters() replaceText = self.replaceEntry.text() control = globalref.mainControl.activeControl if control.replaceInCurrentNode(searchText, regExpObj, typeName, fieldName, replaceText): self.find() else: QMessageBox.warning(self, 'TreeLine', _('Error - replacement failed')) self.matchedSpot = None self.updateAvail() def replaceAll(self): """Replace all text matches. """ searchText, regExpObj, typeName, fieldName = self.findParameters() replaceText = self.replaceEntry.text() control = globalref.mainControl.activeControl qty = control.replaceAll(searchText, regExpObj, typeName, fieldName, replaceText) self.matchedSpot = None self.updateAvail() self.resultLabel.setText(_('Replaced {0} matches').format(qty)) def closeEvent(self, event): """Signal that the dialog is closing. Arguments: event -- the close event """ self.dialogShown.emit(False) SortWhat = enum.IntEnum('SortWhat', 'fullTree selectBranch selectChildren selectSiblings') SortMethod = enum.IntEnum('SortMethod', 'fieldSort titleSort') SortDirection = enum.IntEnum('SortDirection', 'forward reverse') class SortDialog(QDialog): """Dialog for defining sort operations. """ dialogShown = pyqtSignal(bool) def __init__(self, parent=None): """Initialize the sort dialog. Arguments: parent -- the parent window """ super().__init__(parent) self.setAttribute(Qt.WA_QuitOnClose, False) self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) self.setWindowTitle(_('Sort Nodes')) topLayout = QVBoxLayout(self) self.setLayout(topLayout) horizLayout = QHBoxLayout() topLayout.addLayout(horizLayout) whatBox = QGroupBox(_('What to Sort')) horizLayout.addWidget(whatBox) whatLayout = QVBoxLayout(whatBox) self.whatButtons = QButtonGroup(self) button = QRadioButton(_('&Entire tree')) self.whatButtons.addButton(button, SortWhat.fullTree) whatLayout.addWidget(button) button = QRadioButton(_('Selected &branches')) self.whatButtons.addButton(button, SortWhat.selectBranch) whatLayout.addWidget(button) button = QRadioButton(_('Selection\'s childre&n')) self.whatButtons.addButton(button, SortWhat.selectChildren) whatLayout.addWidget(button) button = QRadioButton(_('Selection\'s &siblings')) self.whatButtons.addButton(button, SortWhat.selectSiblings) whatLayout.addWidget(button) self.whatButtons.button(SortWhat.fullTree).setChecked(True) vertLayout = QVBoxLayout() horizLayout.addLayout(vertLayout) methodBox = QGroupBox(_('Sort Method')) vertLayout.addWidget(methodBox) methodLayout = QVBoxLayout(methodBox) self.methodButtons = QButtonGroup(self) button = QRadioButton(_('&Predefined Key Fields')) self.methodButtons.addButton(button, SortMethod.fieldSort) methodLayout.addWidget(button) button = QRadioButton(_('Node &Titles')) self.methodButtons.addButton(button, SortMethod.titleSort) methodLayout.addWidget(button) self.methodButtons.button(SortMethod.fieldSort).setChecked(True) directionBox = QGroupBox(_('Sort Direction')) vertLayout.addWidget(directionBox) directionLayout = QVBoxLayout(directionBox) self.directionButtons = QButtonGroup(self) button = QRadioButton(_('&Forward')) self.directionButtons.addButton(button, SortDirection.forward) directionLayout.addWidget(button) button = QRadioButton(_('&Reverse')) self.directionButtons.addButton(button, SortDirection.reverse) directionLayout.addWidget(button) self.directionButtons.button(SortDirection.forward).setChecked(True) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch() okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.sortAndClose) applyButton = QPushButton(_('&Apply')) ctrlLayout.addWidget(applyButton) applyButton.clicked.connect(self.sortNodes) closeButton = QPushButton(_('&Close')) ctrlLayout.addWidget(closeButton) closeButton.clicked.connect(self.close) self.updateCommandsAvail() def updateCommandsAvail(self): """Set what to sort options available based on tree selections. """ selModel = globalref.mainControl.activeControl.currentSelectionModel() hasChild = False hasSibling = False for spot in selModel.selectedSpots(): if spot.nodeRef.childList: hasChild = True if spot.parentSpot and len(spot.parentSpot.nodeRef.childList) > 1: hasSibling = True self.whatButtons.button(SortWhat.selectBranch).setEnabled(hasChild) self.whatButtons.button(SortWhat.selectChildren).setEnabled(hasChild) self.whatButtons.button(SortWhat.selectSiblings).setEnabled(hasSibling) if not self.whatButtons.checkedButton().isEnabled(): self.whatButtons.button(SortWhat.fullTree).setChecked(True) def sortNodes(self): """Perform the sorting operation. """ QApplication.setOverrideCursor(Qt.WaitCursor) control = globalref.mainControl.activeControl selSpots = control.currentSelectionModel().selectedSpots() if self.whatButtons.checkedId() == SortWhat.fullTree: selSpots = [control.structure.spotByNumber(0)] elif self.whatButtons.checkedId() == SortWhat.selectSiblings: selSpots = [spot.parentSpot for spot in selSpots] if self.whatButtons.checkedId() in (SortWhat.fullTree, SortWhat.selectBranch): rootSpots = selSpots[:] selSpots = [] for root in rootSpots: for spot in root.spotDescendantGen(): if spot.nodeRef.childList: selSpots.append(spot) undo.ChildListUndo(control.structure.undoList, [spot.nodeRef for spot in selSpots]) forward = self.directionButtons.checkedId() == SortDirection.forward if self.methodButtons.checkedId() == SortMethod.fieldSort: for spot in selSpots: spot.nodeRef.sortChildrenByField(False, forward) # reset temporary sort field storage for nodeFormat in control.structure.treeFormats.values(): nodeFormat.sortFields = [] else: for spot in selSpots: spot.nodeRef.sortChildrenByTitle(False, forward) control.updateAll() QApplication.restoreOverrideCursor() def sortAndClose(self): """Perform the sorting operation and close the dialog. """ self.sortNodes() self.close() def closeEvent(self, event): """Signal that the dialog is closing. Arguments: event -- the close event """ self.dialogShown.emit(False) NumberingScope = enum.IntEnum('NumberingScope', 'fullTree selectBranch selectChildren') NumberingNoField = enum.IntEnum('NumberingNoField', 'ignoreNoField restartAfterNoField reserveNoField') class NumberingDialog(QDialog): """Dialog for updating node nuumbering fields. """ dialogShown = pyqtSignal(bool) def __init__(self, parent=None): """Initialize the numbering dialog. Arguments: parent -- the parent window """ super().__init__(parent) self.setAttribute(Qt.WA_QuitOnClose, False) self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) self.setWindowTitle(_('Update Node Numbering')) topLayout = QVBoxLayout(self) self.setLayout(topLayout) whatBox = QGroupBox(_('What to Update')) topLayout.addWidget(whatBox) whatLayout = QVBoxLayout(whatBox) self.whatButtons = QButtonGroup(self) button = QRadioButton(_('&Entire tree')) self.whatButtons.addButton(button, NumberingScope.fullTree) whatLayout.addWidget(button) button = QRadioButton(_('Selected &branches')) self.whatButtons.addButton(button, NumberingScope.selectBranch) whatLayout.addWidget(button) button = QRadioButton(_('&Selection\'s children')) self.whatButtons.addButton(button, NumberingScope.selectChildren) whatLayout.addWidget(button) self.whatButtons.button(NumberingScope.fullTree).setChecked(True) rootBox = QGroupBox(_('Root Node')) topLayout.addWidget(rootBox) rootLayout = QVBoxLayout(rootBox) self.rootCheck = QCheckBox(_('Include top-level nodes')) rootLayout.addWidget(self.rootCheck) self.rootCheck.setChecked(True) noFieldBox = QGroupBox(_('Handling Nodes without Numbering ' 'Fields')) topLayout.addWidget(noFieldBox) noFieldLayout = QVBoxLayout(noFieldBox) self.noFieldButtons = QButtonGroup(self) button = QRadioButton(_('&Ignore and skip')) self.noFieldButtons.addButton(button, NumberingNoField.ignoreNoField) noFieldLayout.addWidget(button) button = QRadioButton(_('&Restart numbers for next siblings')) self.noFieldButtons.addButton(button, NumberingNoField.restartAfterNoField) noFieldLayout.addWidget(button) button = QRadioButton(_('Reserve &numbers')) self.noFieldButtons.addButton(button, NumberingNoField.reserveNoField) noFieldLayout.addWidget(button) self.noFieldButtons.button(NumberingNoField. ignoreNoField).setChecked(True) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch() okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.numberAndClose) applyButton = QPushButton(_('&Apply')) ctrlLayout.addWidget(applyButton) applyButton.clicked.connect(self.updateNumbering) closeButton = QPushButton(_('&Close')) ctrlLayout.addWidget(closeButton) closeButton.clicked.connect(self.close) self.updateCommandsAvail() def updateCommandsAvail(self): """Set branch numbering available based on tree selections. """ selNodes = globalref.mainControl.activeControl.currentSelectionModel() hasChild = False for node in selNodes.selectedNodes(): if node.childList: hasChild = True self.whatButtons.button(NumberingScope. selectChildren).setEnabled(hasChild) if not self.whatButtons.checkedButton().isEnabled(): self.whatButtons.button(NumberingScope.fullTree).setChecked(True) def checkForNumberingFields(self): """Check that the tree formats have numbering formats. Return a dict of numbering field names by node format name. If not found, warn user. """ fieldDict = (globalref.mainControl.activeControl.structure.treeFormats. numberingFieldDict()) if not fieldDict: QMessageBox.warning(self, _('TreeLine Numbering'), _('No numbering fields were found in data types')) return fieldDict def updateNumbering(self): """Perform the numbering update operation. """ QApplication.setOverrideCursor(Qt.WaitCursor) fieldDict = self.checkForNumberingFields() if fieldDict: control = globalref.mainControl.activeControl selNodes = control.currentSelectionModel().selectedNodes() if (self.whatButtons.checkedId() == NumberingScope.fullTree or len(selNodes) == 0): selNodes = control.structure.childList undo.DataUndo(control.structure.undoList, selNodes, addBranch=True) reserveNums = (self.noFieldButtons.checkedId() == NumberingNoField.reserveNoField) restartSetting = (self.noFieldButtons.checkedId() == NumberingNoField.restartAfterNoField) includeRoot = self.rootCheck.isChecked() if self.whatButtons.checkedId() == NumberingScope.selectChildren: levelLimit = 2 else: levelLimit = sys.maxsize startNum = [1] completedClones = set() for node in selNodes: node.updateNumbering(fieldDict, startNum, levelLimit, completedClones, includeRoot, reserveNums, restartSetting) control.updateAll() QApplication.restoreOverrideCursor() def numberAndClose(self): """Perform the numbering update operation and close the dialog. """ self.updateNumbering() self.close() def closeEvent(self, event): """Signal that the dialog is closing. Arguments: event -- the close event """ self.dialogShown.emit(False) menuNames = collections.OrderedDict([(N_('File Menu'), _('File')), (N_('Edit Menu'), _('Edit')), (N_('Node Menu'), _('Node')), (N_('Data Menu'), _('Data')), (N_('Tools Menu'), _('Tools')), (N_('Format Menu'), _('Format')), (N_('View Menu'), _('View')), (N_('Window Menu'), _('Window')), (N_('Help Menu'), _('Help'))]) class CustomShortcutsDialog(QDialog): """Dialog for customizing keyboard commands. """ def __init__(self, allActions, parent=None): """Create a shortcuts selection dialog. Arguments: allActions -- dict of all actions from a window parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Keyboard Shortcuts')) topLayout = QVBoxLayout(self) self.setLayout(topLayout) scrollArea = QScrollArea() topLayout.addWidget(scrollArea) viewport = QWidget() viewLayout = QGridLayout(viewport) scrollArea.setWidget(viewport) scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scrollArea.setWidgetResizable(True) self.editors = [] for i, keyOption in enumerate(globalref.keyboardOptions.values()): category = menuNames.get(keyOption.category, _('No menu')) try: action = allActions[keyOption.name] except KeyError: pass else: text = '{0} > {1}'.format(category, action.toolTip()) label = QLabel(text) viewLayout.addWidget(label, i, 0) editor = KeyLineEdit(keyOption, action, self) viewLayout.addWidget(editor, i, 1) self.editors.append(editor) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) restoreButton = QPushButton(_('&Restore Defaults')) ctrlLayout.addWidget(restoreButton) restoreButton.clicked.connect(self.restoreDefaults) ctrlLayout.addStretch(0) self.okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.editors[0].setFocus() def restoreDefaults(self): """Restore all default keyboard shortcuts. """ for editor in self.editors: editor.loadDefaultKey() def accept(self): """Save any changes to options and actions before closing. """ modified = False for editor in self.editors: if editor.modified: editor.saveChange() modified = True if modified: globalref.keyboardOptions.writeFile() super().accept() class KeyLineEdit(QLineEdit): """Line editor for keyboad sequence entry. """ usedKeySet = set() blankText = ' ' * 8 def __init__(self, keyOption, action, parent=None): """Create a key editor. Arguments: keyOption -- the KeyOptionItem for this editor action -- the action to update on changes parent -- the parent dialog """ super().__init__(parent) self.keyOption = keyOption self.keyAction = action self.key = None self.modified = False self.setReadOnly(True) self.loadKey() def loadKey(self): """Load the initial key shortcut from the option. """ key = self.keyOption.value if key: self.setKey(key) else: self.setText(KeyLineEdit.blankText) def loadDefaultKey(self): """Change to the default key shortcut from the option. Arguments: useDefault -- if True, load the default key """ key = self.keyOption.defaultValue if key == self.key: return if key: self.setKey(key) self.modified = True else: self.clearKey(False) def setKey(self, key): """Set this editor to the given key and add to the used key set. Arguments: key - the QKeySequence to add """ keyText = key.toString(QKeySequence.NativeText) self.setText(keyText) self.key = key KeyLineEdit.usedKeySet.add(keyText) def clearKey(self, staySelected=True): """Remove any existing key. """ self.setText(KeyLineEdit.blankText) if staySelected: self.selectAll() if self.key: KeyLineEdit.usedKeySet.remove(self.key.toString(QKeySequence. NativeText)) self.key = None self.modified = True def saveChange(self): """Save any change to the option and action. """ if self.modified: self.keyOption.setValue(self.key) if self.key: self.keyAction.setShortcut(self.key) else: self.keyAction.setShortcut(QKeySequence()) def keyPressEvent(self, event): """Capture key strokes and update the editor if valid. Arguments: event -- the key press event """ if event.key() in (Qt.Key_Shift, Qt.Key_Control, Qt.Key_Meta, Qt.Key_Alt, Qt.Key_AltGr, Qt.Key_CapsLock, Qt.Key_NumLock, Qt.Key_ScrollLock, Qt.Key_Pause, Qt.Key_Print, Qt.Key_Cancel): event.ignore() elif event.key() in (Qt.Key_Backspace, Qt.Key_Escape): self.clearKey() event.accept() else: modifier = event.modifiers() if modifier & Qt.KeypadModifier: modifier = modifier ^ Qt.KeypadModifier key = QKeySequence(event.key() + int(modifier)) if key != self.key: keyText = key.toString(QKeySequence.NativeText) if keyText not in KeyLineEdit.usedKeySet: if self.key: KeyLineEdit.usedKeySet.remove(self.key. toString(QKeySequence. NativeText)) self.setKey(key) self.selectAll() self.modified = True else: text = _('Key {0} is already used').format(keyText) QMessageBox.warning(self.parent(), 'TreeLine', text) event.accept() def contextMenuEvent(self, event): """Change to a context menu with a clear command. Arguments: event -- the menu event """ menu = QMenu(self) menu.addAction(_('Clear &Key'), self.clearKey) menu.exec_(event.globalPos()) def mousePressEvent(self, event): """Capture mouse clicks to avoid selection loss. Arguments: event -- the mouse event """ event.accept() def mouseReleaseEvent(self, event): """Capture mouse clicks to avoid selection loss. Arguments: event -- the mouse event """ event.accept() def mouseMoveEvent(self, event): """Capture mouse clicks to avoid selection loss. Arguments: event -- the mouse event """ event.accept() def mouseDoubleClickEvent(self, event): """Capture mouse clicks to avoid selection loss. Arguments: event -- the mouse event """ event.accept() def focusInEvent(self, event): """Select contents when focussed. Arguments: event -- the focus event """ self.selectAll() super().focusInEvent(event) class CustomToolbarDialog(QDialog): """Dialog for customizing toolbar buttons. """ separatorString = _('--Separator--') def __init__(self, allActions, updateFunction, parent=None): """Create a toolbar buttons customization dialog. Arguments: allActions -- dict of all actions from a window updateFunction -- a function ref for updating window toolbars parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Customize Toolbars')) self.allActions = allActions self.updateFunction = updateFunction self.availableCommands = [] self.modified = False self.numToolbars = 0 self.availableCommands = [] self.toolbarLists = [] topLayout = QVBoxLayout(self) self.setLayout(topLayout) gridLayout = QGridLayout() topLayout.addLayout(gridLayout) sizeBox = QGroupBox(_('Toolbar &Size')) gridLayout.addWidget(sizeBox, 0, 0, 1, 2) sizeLayout = QVBoxLayout(sizeBox) self.sizeCombo = QComboBox() sizeLayout.addWidget(self.sizeCombo) self.sizeCombo.addItems([_('Small Icons'), _('Large Icons')]) self.sizeCombo.currentIndexChanged.connect(self.setModified) numberBox = QGroupBox(_('Toolbar Quantity')) gridLayout.addWidget(numberBox, 0, 2) numberLayout = QHBoxLayout(numberBox) self.quantitySpin = QSpinBox() numberLayout.addWidget(self.quantitySpin) self.quantitySpin.setRange(0, 20) numberlabel = QLabel(_('&Toolbars')) numberLayout.addWidget(numberlabel) numberlabel.setBuddy(self.quantitySpin) self.quantitySpin.valueChanged.connect(self.changeQuantity) availableBox = QGroupBox(_('A&vailable Commands')) gridLayout.addWidget(availableBox, 1, 0) availableLayout = QVBoxLayout(availableBox) menuCombo = QComboBox() availableLayout.addWidget(menuCombo) menuCombo.addItems([_(name) for name in menuNames.keys()]) menuCombo.currentIndexChanged.connect(self.updateAvailableCommands) self.availableListWidget = QListWidget() availableLayout.addWidget(self.availableListWidget) buttonLayout = QVBoxLayout() gridLayout.addLayout(buttonLayout, 1, 1) self.addButton = QPushButton('>>') buttonLayout.addWidget(self.addButton) self.addButton.setMaximumWidth(self.addButton.sizeHint().height()) self.addButton.clicked.connect(self.addTool) self.removeButton = QPushButton('<<') buttonLayout.addWidget(self.removeButton) self.removeButton.setMaximumWidth(self.removeButton.sizeHint(). height()) self.removeButton.clicked.connect(self.removeTool) toolbarBox = QGroupBox(_('Tool&bar Commands')) gridLayout.addWidget(toolbarBox, 1, 2) toolbarLayout = QVBoxLayout(toolbarBox) self.toolbarCombo = QComboBox() toolbarLayout.addWidget(self.toolbarCombo) self.toolbarCombo.currentIndexChanged.connect(self. updateToolbarCommands) self.toolbarListWidget = QListWidget() toolbarLayout.addWidget(self.toolbarListWidget) self.toolbarListWidget.currentRowChanged.connect(self. setButtonsAvailable) moveLayout = QHBoxLayout() toolbarLayout.addLayout(moveLayout) self.moveUpButton = QPushButton(_('Move &Up')) moveLayout.addWidget(self.moveUpButton) self.moveUpButton.clicked.connect(self.moveUp) self.moveDownButton = QPushButton(_('Move &Down')) moveLayout.addWidget(self.moveDownButton) self.moveDownButton.clicked.connect(self.moveDown) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) restoreButton = QPushButton(_('&Restore Defaults')) ctrlLayout.addWidget(restoreButton) restoreButton.clicked.connect(self.restoreDefaults) ctrlLayout.addStretch() self.okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.clicked.connect(self.accept) self.applyButton = QPushButton(_('&Apply')) ctrlLayout.addWidget(self.applyButton) self.applyButton.clicked.connect(self.applyChanges) self.applyButton.setEnabled(False) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.updateAvailableCommands(0) self.loadToolbars() def setModified(self): """Set modified flag and make apply button available. """ self.modified = True self.applyButton.setEnabled(True) def setButtonsAvailable(self): """Enable or disable buttons based on toolbar list state. """ toolbarNum = numCommands = commandNum = 0 if self.numToolbars: toolbarNum = self.toolbarCombo.currentIndex() numCommands = len(self.toolbarLists[toolbarNum]) if self.toolbarLists[toolbarNum]: commandNum = self.toolbarListWidget.currentRow() self.addButton.setEnabled(self.numToolbars > 0) self.removeButton.setEnabled(self.numToolbars and numCommands) self.moveUpButton.setEnabled(self.numToolbars and numCommands > 1 and commandNum > 0) self.moveDownButton.setEnabled(self.numToolbars and numCommands > 1 and commandNum < numCommands - 1) def loadToolbars(self, defaultOnly=False): """Load all toolbar data from options. Arguments: defaultOnly -- if True, load default settings """ size = (globalref.toolbarOptions['ToolbarSize'] if not defaultOnly else globalref.toolbarOptions.getDefaultValue('ToolbarSize')) self.sizeCombo.blockSignals(True) if size < 24: self.sizeCombo.setCurrentIndex(0) else: self.sizeCombo.setCurrentIndex(1) self.sizeCombo.blockSignals(False) self.numToolbars = (globalref.toolbarOptions['ToolbarQuantity'] if not defaultOnly else globalref.toolbarOptions. getDefaultValue('ToolbarQuantity')) self.quantitySpin.blockSignals(True) self.quantitySpin.setValue(self.numToolbars) self.quantitySpin.blockSignals(False) self.toolbarLists = [] commands = (globalref.toolbarOptions['ToolbarCommands'] if not defaultOnly else globalref.toolbarOptions. getDefaultValue('ToolbarCommands')) self.toolbarLists = [cmd.split(',') for cmd in commands] # account for toolbar quantity mismatch (should not happen) del self.toolbarLists[self.numToolbars:] while len(self.toolbarLists) < self.numToolbars: self.toolbarLists.append([]) self.updateToolbarCombo() def updateToolbarCombo(self): """Fill combo with toolbar numbers for current quantity. """ self.toolbarCombo.clear() if self.numToolbars: self.toolbarCombo.addItems(['Toolbar {0}'.format(num + 1) for num in range(self.numToolbars)]) else: self.toolbarListWidget.clear() self.setButtonsAvailable() def updateAvailableCommands(self, menuNum): """Fill in available command list for given menu. Arguments: menuNum -- the index of the current menu selected """ menuName = list(menuNames.keys())[menuNum] self.availableCommands = [] self.availableListWidget.clear() for option in globalref.keyboardOptions.values(): if option.category == menuName: action = self.allActions[option.name] icon = action.icon() if not icon.isNull(): self.availableCommands.append(option.name) QListWidgetItem(icon, action.toolTip(), self.availableListWidget) QListWidgetItem(CustomToolbarDialog.separatorString, self.availableListWidget) self.availableListWidget.setCurrentRow(0) def updateToolbarCommands(self, toolbarNum): """Fill in toolbar commands for given toolbar. Arguments: toolbarNum -- the number of the toolbar to update """ self.toolbarListWidget.clear() if self.numToolbars == 0: return for command in self.toolbarLists[toolbarNum]: if command: action = self.allActions[command] QListWidgetItem(action.icon(), action.toolTip(), self.toolbarListWidget) else: # separator QListWidgetItem(CustomToolbarDialog.separatorString, self.toolbarListWidget) if self.toolbarLists[toolbarNum]: self.toolbarListWidget.setCurrentRow(0) self.setButtonsAvailable() def changeQuantity(self, qty): """Change the toolbar quantity based on a spin box signal. Arguments: qty -- the new toolbar quantity """ self.numToolbars = qty while qty > len(self.toolbarLists): self.toolbarLists.append([]) self.updateToolbarCombo() self.setModified() def addTool(self): """Add the selected command to the current toolbar. """ toolbarNum = self.toolbarCombo.currentIndex() try: command = self.availableCommands[self.availableListWidget. currentRow()] action = self.allActions[command] item = QListWidgetItem(action.icon(), action.toolTip()) except IndexError: command = '' item = QListWidgetItem(CustomToolbarDialog.separatorString) if self.toolbarLists[toolbarNum]: pos = self.toolbarListWidget.currentRow() + 1 else: pos = 0 self.toolbarLists[toolbarNum].insert(pos, command) self.toolbarListWidget.insertItem(pos, item) self.toolbarListWidget.setCurrentRow(pos) self.toolbarListWidget.scrollToItem(item) self.setModified() def removeTool(self): """Remove the selected command from the current toolbar. """ toolbarNum = self.toolbarCombo.currentIndex() pos = self.toolbarListWidget.currentRow() del self.toolbarLists[toolbarNum][pos] self.toolbarListWidget.takeItem(pos) if self.toolbarLists[toolbarNum]: if pos == len(self.toolbarLists[toolbarNum]): pos -= 1 self.toolbarListWidget.setCurrentRow(pos) self.setModified() def moveUp(self): """Raise the selected command. """ toolbarNum = self.toolbarCombo.currentIndex() pos = self.toolbarListWidget.currentRow() command = self.toolbarLists[toolbarNum].pop(pos) self.toolbarLists[toolbarNum].insert(pos - 1, command) item = self.toolbarListWidget.takeItem(pos) self.toolbarListWidget.insertItem(pos - 1, item) self.toolbarListWidget.setCurrentRow(pos - 1) self.toolbarListWidget.scrollToItem(item) self.setModified() def moveDown(self): """Lower the selected command. """ toolbarNum = self.toolbarCombo.currentIndex() pos = self.toolbarListWidget.currentRow() command = self.toolbarLists[toolbarNum].pop(pos) self.toolbarLists[toolbarNum].insert(pos + 1, command) item = self.toolbarListWidget.takeItem(pos) self.toolbarListWidget.insertItem(pos + 1, item) self.toolbarListWidget.setCurrentRow(pos + 1) self.toolbarListWidget.scrollToItem(item) self.setModified() def restoreDefaults(self): """Restore all default toolbar settings. """ self.loadToolbars(True) self.setModified() def applyChanges(self): """Apply any changes from the dialog. """ size = 16 if self.sizeCombo.currentIndex() == 0 else 32 globalref.toolbarOptions.changeValue('ToolbarSize', size) globalref.toolbarOptions.changeValue('ToolbarQuantity', self.numToolbars) del self.toolbarLists[self.numToolbars:] commands = [','.join(cmds) for cmds in self.toolbarLists] globalref.toolbarOptions.changeValue('ToolbarCommands', commands) globalref.toolbarOptions.writeFile() self.modified = False self.applyButton.setEnabled(False) self.updateFunction() def accept(self): """Apply changes and close the dialog. """ if self.modified: self.applyChanges() super().accept() class CustomFontData: """Class to store custom font settings. Acts as a stand-in for PrintData class in the font page of the dialog. """ def __init__(self, fontOption, useAppDefault=True): """Initialize the font data. Arguments: fontOption -- the name of the font setting to retrieve useAppDefault -- use app default if true, o/w use sys default """ self.fontOption = fontOption if useAppDefault: self.defaultFont = QTextDocument().defaultFont() else: self.defaultFont = QFont(globalref.mainControl.systemFont) self.useDefaultFont = True self.mainFont = QFont(self.defaultFont) fontName = globalref.miscOptions[self.fontOption] if fontName: self.mainFont.fromString(fontName) self.useDefaultFont = False def recordChanges(self): """Record the updated font info to the option settings. """ if self.useDefaultFont: globalref.miscOptions.changeValue(self.fontOption, '') else: globalref.miscOptions.changeValue(self.fontOption, self.mainFont.toString()) class CustomFontDialog(QDialog): """Dialog for selecting custom fonts. Uses the print setup dialog's font page for the details. """ updateRequired = pyqtSignal() def __init__(self, parent=None): """Create a font customization dialog. Arguments: parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Customize Fonts')) topLayout = QVBoxLayout(self) self.setLayout(topLayout) self.tabs = QTabWidget() topLayout.addWidget(self.tabs) self.tabs.setUsesScrollButtons(False) self.tabs.currentChanged.connect(self.updateTabDefault) self.pages = [] defaultLabel = _('&Use system default font') appFontPage = printdialogs.FontPage(CustomFontData('AppFont', False), defaultLabel) self.pages.append(appFontPage) self.tabs.addTab(appFontPage, _('App Default Font')) defaultLabel = _('&Use app default font') treeFontPage = printdialogs.FontPage(CustomFontData('TreeFont'), defaultLabel) self.pages.append(treeFontPage) self.tabs.addTab(treeFontPage, _('Tree View Font')) outputFontPage = printdialogs.FontPage(CustomFontData('OutputFont'), defaultLabel) self.pages.append(outputFontPage) self.tabs.addTab(outputFontPage, _('Output View Font')) editorFontPage = printdialogs.FontPage(CustomFontData('EditorFont'), defaultLabel) self.pages.append(editorFontPage) self.tabs.addTab(editorFontPage, _('Editor View Font')) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch() self.okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.clicked.connect(self.accept) self.applyButton = QPushButton(_('&Apply')) ctrlLayout.addWidget(self.applyButton) self.applyButton.clicked.connect(self.applyChanges) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) def updateTabDefault(self): """Update the default font on the newly shown page. """ appFontWidget = self.tabs.widget(0) currentWidget = self.tabs.currentWidget() if appFontWidget is not currentWidget: if appFontWidget.defaultCheck.isChecked(): defaultFont = QFont(globalref.mainControl.systemFont) else: defaultFont = appFontWidget.readFont() if defaultFont: currentWidget.printData.defaultFont = defaultFont if currentWidget.defaultCheck.isChecked(): currentWidget.printData.mainFont = QFont(defaultFont) currentWidget.currentFont = (currentWidget.printData. mainFont) currentWidget.setFont(defaultFont) def applyChanges(self): """Apply any changes from the dialog. """ modified = False for page in self.pages: if page.saveChanges(): page.printData.recordChanges() modified = True if modified: globalref.miscOptions.writeFile() self.updateRequired.emit() def accept(self): """Apply changes and close the dialog. """ self.applyChanges() super().accept() class AboutDialog(QDialog): """Show program info in a text box. """ def __init__(self, title, textLines, icon=None, parent=None): """Create the dialog. Arguments: title -- the window title text textLines -- a list of lines to show icon -- an icon to show if given parent -- the parent window """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(title) topLayout = QVBoxLayout(self) self.setLayout(topLayout) mainLayout = QHBoxLayout() topLayout.addLayout(mainLayout) iconLabel = QLabel() iconLabel.setPixmap(icon.pixmap(128, 128)) mainLayout.addWidget(iconLabel) textBox = QPlainTextEdit() textBox.setReadOnly(True) textBox.setWordWrapMode(QTextOption.NoWrap) textBox.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) textBox.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) text = '\n'.join(textLines) textBox.setPlainText(text) size = textBox.fontMetrics().size(0, text) size.setHeight(size.height() + 10) size.setWidth(size.width() + 10) textBox.setMinimumSize(size) mainLayout.addWidget(textBox) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch() okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) TreeLine/source/gennumber.py0000644000175000017500000002466013363127527015120 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # gennumber.py, provides a class for number formating # # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import math class GenNumber: """Class to store & format number values. Uses a simple syntax (sequence of '#', '0', etc.) for formatting. """ def __init__(self, num=0): """Initialize a GenNumber object with a number, string or a GenNumber. Raises ValueError with an inappropriate argument. Accepts one of the following arguments as num 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 an int, float, string or a GenNumber. Raises ValueError with an inappropriate argument. Arguments: num -- the value in int, float, string or GenNumber format """ try: self.num = int(str(num)) except ValueError: self.num = float(str(num)) def setFromStr(self, numStr, strFormat='#\,###'): """Set number value based on given format string. Removes the extra characters from format and uses format's radix char. Returns self. Arguments: numStr -- the string to evaluate strFormat -- the format to use to interpret the number string """ radix = _getRadix(strFormat) strFormat = _unescapeFormat(radix, strFormat).strip() extraChar = re.sub(r'[#0\seE\-\+{}]'.format(re.escape(radix)), '', strFormat) if extraChar: numStr = re.sub('[{}]'.format(re.escape(extraChar)), '', numStr) if radix == ',': numStr = numStr.replace(',', '.') self.setNumber(numStr) return self def numStr(self, strFormat='#.##'): """Return the number string in the given format, including exponents. Format: # = optional digit 0 = required digit e = exponent - = optional sign + = required sign space (external) = digit or space space (internal) = thousands sep \, = thousands separator \. = thousands separator Arguments: strFormat -- format for number export """ formMain, formExp = _doubleSplit('eE', strFormat) if not formExp: return self.basicNumStr(strFormat) exp = math.floor(math.log10(abs(self.num))) num = self.num / 10**exp totPlcs = len(re.findall(r'[#0]', formMain)) num = round(num, totPlcs - 1 if totPlcs > 0 else 0) radix = _getRadix(strFormat) wholePlcs = len(re.findall(r'[#0]', _doubleSplit(radix, formMain)[0])) expChg = wholePlcs - int(math.floor(math.log10(abs(num)))) - 1 num = num * 10**expChg exp -= expChg c = 'e' if 'e' in strFormat else 'E' return '{0}{1}{2}'.format(GenNumber(num).basicNumStr(formMain), c, GenNumber(exp).basicNumStr(formExp)) def basicNumStr(self, strFormat='#.##'): """Return number string in the given format, without exponent support. Format: # = optional digit 0 = required digit - = optional sign + = required sign space (external) = digit or space space (internal) = thousands sep \, = thousands separator \. = thousands separator Arguments: strFormat -- format for number export """ radix = _getRadix(strFormat) strFormat = _unescapeFormat(radix, strFormat) formWhole, formFract = _doubleSplit(radix, strFormat) decPlcs = len(re.findall(r'[#0]', formFract)) numWhole, numFract = _doubleSplit('.', '{0:.{1}f}'.format(self.num, decPlcs)) numFract = numFract.rstrip('0') 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.pop() if formWhole else '' 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 (strFormat and strFormat[-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 __eq__(self, other): """Equality test. """ try: return self.num == other.num except AttributeError: return self.num == other def __ne__(self, other): """Non-equality test. """ try: return self.num != other.num except AttributeError: return self.num != other def __lt__(self, other): """Less than test. """ try: return self.num < other.num except AttributeError: return self.num < other def __gt__(self, other): """Greater than test. """ try: return self.num > other.num except AttributeError: return self.num > other def __le__(self, other): """Less than or equal to test. """ try: return self.num <= other.num except AttributeError: return self.num <= other def __ge__(self, other): """Greater than or equal to test. """ try: return self.num >= other.num except AttributeError: return self.num >= other def __add__(self, other): """Addition operator. """ try: return self.num + other.num except AttributeError: return self.num + other def __radd__(self, other): """Reverse addition operator. """ return other + self.num def __sub__(self, other): """Subtraction operator. """ try: return self.num - other.num except AttributeError: return self.num - other def __rsub__(self, other): """Reverse subtraction operator. """ return other - self.num def __mul__(self, other): """Multiplication operator. """ try: return self.num * other.num except AttributeError: return self.num * other def __rmul__(self, other): """Reverse multiplication operator. """ return other * self.num def __truediv__(self, other): """True division operator. """ try: return self.num / other.num except AttributeError: return self.num / other def __rtruediv__(self, other): """Reverse true division operator. """ return other / self.num def __floordiv__(self, other): """Floor division operator. """ try: return self.num // other.num except AttributeError: return self.num // other def __rfloordiv__(self, other): """Reverse floor division operator. """ return other // self.num def __int__(self): """Return integer value. """ return int(self.num) def __float__(self): """Return float value. """ return float(self.num) def __round__(self): """Return rounded value. """ return round(self.num) def __hash__(self): """Allow use as dictionary key. """ return hash(self.num) ######### Utility Functions ########## def _doubleSplit(sepChars, string): """Return tuple of string split in two, separated by one of sepChars. Returns a tuple, size 2, with the second entry empty if no sep found. Arguments: sepChars -- a string of separator characters string -- the string to split """ for sep in sepChars: result = string.split(sep, 1) if len(result) == 2: return result return (string, '') def _getRadix(strFormat): """Return the radix character (. or ,) used in format. Infers from use of slashed separators and non-slashed radix. Assumes radix is "." if ambiguous. Arguments: strFormat -- the string format to evaluate """ if not '\,' in strFormat and ('\.' in strFormat or (',' in strFormat and not '.' in strFormat)): return ',' return '.' def _unescapeFormat(radix, strFormat): """Return format with escapes removed from non-radix separators. Arguments: radix -- the current radix character strFormat - the string format to modify """ if radix == '.': return strFormat.replace('\,', ',') return strFormat.replace('\.', '.') TreeLine/source/p3.py0000644000175000017500000001215213745100215013436 0ustar dougdoug#!/usr/bin/env python3 # 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 $. # Modified by Doug Bell to be compatible with Python 3 from array import array from time import time import struct import hashlib shaHash = hashlib.sha1 class CryptError(Exception): pass def _hash(text): return shaHash(text).digest() _ivlen = 16 _maclen = 8 _state = _hash(repr(time()).encode()) # added by Doug Bell for compatibility with 64-bit systems _arraytype = [t for t in 'LIH' if struct.calcsize(t) == 4][0] try: import os _pid = repr(os.getpid()).encode() except (ImportError, AttributeError): _pid = '' def _expand_key(key, clen): blocks = (clen+19)//20 xkey=[] seed=key for i in range(blocks): seed=shaHash(key+seed).digest() xkey.append(seed) j = b''.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 = b'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 = [repr(time()).encode(), _pid, _state, repr(len(plain)).encode(),plain, key] nonce = H(b','.join(nlist))[:_ivlen] _state = H(b'update2'+_state+nonce) k_enc, k_auth = H(b'enc'+key+nonce), H(b'auth'+key+nonce) n=len(plain) # cipher size not counting IV stream = array(_arraytype, plain+b'0000'[n&3:]) # pad to fill 32-bit words xkey = _expand_key(k_enc, n+4) for i in range(len(stream)): stream[i] = stream[i] ^ xkey[i] ct = nonce + stream.tobytes()[: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]+b'0000'[n&3:],cipher[-_maclen:] k_enc, k_auth = H(b'enc'+key+nonce), H(b'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 range (len(stream)): stream[i] = stream[i] ^ xkey[i] plain = stream.tobytes()[: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). def _hmac_setup(): global _ipad, _opad, _itrans, _otrans _itrans = array('B',[0]*256) _otrans = array('B',[0]*256) for i in range(256): _itrans[i] = i ^ 0x36 _otrans[i] = i ^ 0x5c _itrans = _itrans.tobytes() _otrans = _otrans.tobytes() _ipad = b'\x36'*64 _opad = b'\x5c'*64 def _hmac(msg, key): if len(key)>64: key=shaHash(key).digest() ki = (key.translate(_itrans)+_ipad)[:64] # inner ko = (key.translate(_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 range(n): p3_encrypt(plain,b"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=b"test plaintext" key = b"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+c2[20]).encode()+c2[21:] # change one ciphertext char try: print(d(c3,key)) # should throw exception print("auth verification failure") except CryptError: pass try: print(d(c2,b'wrong key')) # should throw exception print("test failure") except CryptError: pass _hmac_setup() # _test() # _speed() # uncomment to run speed test TreeLine/source/treeformats.py0000644000175000017500000004530113707347077015472 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treeformats.py, provides a class to store node format types and info # # TreeLine, an information storage program # Copyright (C) 2019, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import operator import copy import xml.sax.saxutils import nodeformat import matheval import conditional import treenode import treestructure defaultTypeName = _('DEFAULT') _showConfRootTypeName = _('FILE') _showConfTypeTypeName = _('TYPE') _showConfTypeTitleFieldName = _('TitleFormat') _showConfTypeOutputFieldName = _('OutputFormat') _showConfTypeSpaceFieldName = _('SpaceBetween') _showConfTypeHtmlFieldName = _('FormatHtml') _showConfTypeBulletsFieldName = _('Bullets') _showConfTypeTableFieldName = _('Table') _showConfTypeChildFieldName = _('ChildType') _showConfTypeIconFieldName = _('Icon') _showConfTypeGenericFieldName = _('GenericType') _showConfTypeConditionFieldName = _('ConditionalRule') _showConfTypeSeparatorFieldName = _('ListSeparator') _showConfTypeChildLimitFieldName = _('ChildTypeLimit') _showConfFieldTypeName = _('FIELD') _showConfFieldTypeFieldName = _('FieldType') _showConfFieldFormatFieldName = _('Format') _showConfFieldPrefixFieldName = _('Prefix') _showConfFieldSuffixFieldName = _('Suffix') _showConfFieldInitFieldName = _('InitialValue') _showConfFieldLinesFieldName = _('NumLines') _showConfFieldSortKeyFieldName = _('SortKeyNum') _showConfFieldSortDirFieldName = _('SortForward') _showConfFieldEvalHtmlFieldName = _('EvalHtml') class TreeFormats(dict): """Class to store node format types and info. Stores node formats by format name in a dictionary. Provides methods to change and update format data. """ def __init__(self, formatList=None, setDefault=False): """Initialize the format storage. Arguments: formatList -- the list of formats' file info setDefault - if true, initializes with a default format """ super().__init__() # new names for types renamed in the config dialog (orig names as keys) self.typeRenameDict = {} # nested dict for fields renamed, keys are type name then orig field self.fieldRenameDict = {} self.conditionalTypes = set() # set of math field names with deleted equations, keys are type names self.emptiedMathDict = {} self.mathFieldRefDict = {} # list of math eval levels, each is a dict by type name with lists of # equation fields self.mathLevelList = [] # for saving all-type find/filter conditionals self.savedConditionText = {} self.fileInfoFormat = nodeformat.FileInfoFormat(self) if formatList: for formatData in formatList: name = formatData['formatname'] self[name] = nodeformat.NodeFormat(name, self, formatData) self.updateDerivedRefs() try: self.updateMathFieldRefs() except matheval.CircularMathError: # can see if types with math fields were copied from a 2nd file # handle the exception to avoid failure at file open print('Warning - Circular math fields detected') if nodeformat.FileInfoFormat.typeName in self: self.fileInfoFormat.duplicateFileInfo(self[nodeformat. FileInfoFormat. typeName]) del self[nodeformat.FileInfoFormat.typeName] if setDefault: self[defaultTypeName] = nodeformat.NodeFormat(defaultTypeName, self, addDefaultField=True) def storeFormats(self): """Return a list of formats stored in JSON data. """ formats = list(self.values()) if self.fileInfoFormat.fieldFormatModified: formats.append(self.fileInfoFormat) return sorted([nodeFormat.storeFormat() for nodeFormat in formats], key=operator.itemgetter('formatname')) def loadGlobalSavedConditions(self, propertyDict): """Load all-type saved conditionals from property dict. Arguments: propertyDict -- a JSON property dict """ for key in propertyDict.keys(): if key.startswith('glob-cond-'): self.savedConditionText[key[10:]] = propertyDict[key] def storeGlobalSavedConditions(self, propertyDict): """Save all-type saved conditionals to property dict. Arguments: propertyDict -- a JSON property dict """ for key, text in self.savedConditionText.items(): propertyDict['glob-cond-' + key] = text return propertyDict def copySettings(self, sourceFormats): """Copy all settings from other type formats to these formats. Copy any new formats and delete any missing formats. Arguments: sourceFormats -- the type formats to copy """ if sourceFormats.typeRenameDict: for oldName, newName in sourceFormats.typeRenameDict.items(): try: self[oldName].name = newName except KeyError: pass # skip if new type is renamed formats = list(self.values()) self.clear() for nodeFormat in formats: self[nodeFormat.name] = nodeFormat sourceFormats.typeRenameDict = {} for name in list(self.keys()): if name in sourceFormats: self[name].copySettings(sourceFormats[name]) else: del self[name] for name in sourceFormats.keys(): if name not in self: self[name] = copy.deepcopy(sourceFormats[name]) if (sourceFormats.fileInfoFormat.fieldFormatModified or self.fileInfoFormat.fieldFormatModified): self.fileInfoFormat.duplicateFileInfo(sourceFormats.fileInfoFormat) def typeNames(self): """Return a sorted list of type names. """ return sorted(list(self.keys())) def updateLineParsing(self): """Update the fields parsed in the output lines for each format type. """ for typeFormat in self.values(): typeFormat.updateLineParsing() def addTypeIfMissing(self, typeFormat): """Add format to available types if not a duplicate. Arguments: typeFormat -- the node format to add """ self.setdefault(typeFormat.name, typeFormat) def fieldNameDict(self): """Return a dictionary of field name sets using type names as keys. """ result = {} for typeFormat in self.values(): result[typeFormat.name] = set(typeFormat.fieldNames()) return result def updateDerivedRefs(self): """Update derived type lists (in generics) & the conditional type set. """ self.conditionalTypes = set() for typeFormat in self.values(): typeFormat.derivedTypes = [] if typeFormat.conditional: self.conditionalTypes.add(typeFormat) if typeFormat.genericType: self.conditionalTypes.add(self[typeFormat.genericType]) for typeFormat in self.values(): if typeFormat.genericType: genericType = self[typeFormat.genericType] genericType.derivedTypes.append(typeFormat) if genericType in self.conditionalTypes: self.conditionalTypes.add(typeFormat) for typeFormat in self.values(): if not typeFormat.genericType and not typeFormat.derivedTypes: typeFormat.conditional = None self.conditionalTypes.discard(typeFormat) def updateMathFieldRefs(self): """Update refs used to cycle thru math field evaluations. """ self.mathFieldRefDict = {} allRecursiveRefs = [] recursiveRefDict = {} matheval.RecursiveEqnRef.recursiveRefDict = recursiveRefDict for typeFormat in self.values(): for field in typeFormat.fields(): if field.typeName == 'Math' and field.equation: recursiveRef = matheval.RecursiveEqnRef(typeFormat.name, field) allRecursiveRefs.append(recursiveRef) recursiveRefDict.setdefault(field.name, []).append(recursiveRef) for fieldRef in field.equation.fieldRefs: fieldRef.eqnNodeTypeName = typeFormat.name fieldRef.eqnFieldName = field.name self.mathFieldRefDict.setdefault(fieldRef.fieldName, []).append(fieldRef) if not allRecursiveRefs: return for ref in allRecursiveRefs: ref.setPriorities() allRecursiveRefs.sort() self.mathLevelList = [{allRecursiveRefs[0].eqnTypeName: [allRecursiveRefs[0]]}] for prevRef, currRef in zip(allRecursiveRefs, allRecursiveRefs[1:]): if currRef.evalSequence == prevRef.evalSequence: if prevRef.evalDirection == matheval.EvalDir.optional: prevRef.evalDirection = currRef.evalDirection elif currRef.evalDirection == matheval.EvalDir.optional: currRef.evalDirection = prevRef.evalDirection if currRef.evalDirection != prevRef.evalDirection: self.mathLevelList.append({}) else: self.mathLevelList.append({}) self.mathLevelList[-1].setdefault(currRef.eqnTypeName, []).append(currRef) def numberingFieldDict(self): """Return a dict of numbering field names by node format name. """ result = {} for typeFormat in self.values(): numberingFields = typeFormat.numberingFieldList() if numberingFields: result[typeFormat.name] = numberingFields return result def commonFields(self, nodes): """Return a list of field names common to all given node formats. Retains the field sequence from one of the types. Arguments: nodes -- the nodes to check for common fields """ formats = set() for node in nodes: formats.add(node.formatRef.name) firstFields = self[formats.pop()].fieldNames() commonFields = set(firstFields) for formatName in formats: commonFields.intersection_update(self[formatName].fieldNames()) return [field for field in firstFields if field in commonFields] def savedConditions(self): """Return a dictionary with saved Conditonals from all type formats. """ savedConditions = {} # all-type conditions for name, text in self.savedConditionText.items(): cond = conditional.Conditional(text) savedConditions[name] = cond # specific type conditions for typeFormat in self.values(): for name, text in typeFormat.savedConditionText.items(): cond = conditional.Conditional(text, typeFormat.name) savedConditions[name] = cond return savedConditions def visualConfigStructure(self, fileName): """Export a TreeLine structure containing the config types and fields. Returns the structure. Arguments: fileName -- the name for the root node """ structure = treestructure.TreeStructure() structure.treeFormats = TreeFormats() rootFormat = nodeformat.NodeFormat(_showConfRootTypeName, structure.treeFormats, addDefaultField=True) structure.treeFormats[rootFormat.name] = rootFormat typeFormat = nodeformat.NodeFormat(_showConfTypeTypeName, structure.treeFormats, addDefaultField=True) typeFormat.addField(_showConfTypeTitleFieldName) typeFormat.addField(_showConfTypeOutputFieldName) typeFormat.addField(_showConfTypeSpaceFieldName, {'fieldtype': 'Boolean'}) typeFormat.addField(_showConfTypeHtmlFieldName, {'fieldtype': 'Boolean'}) typeFormat.addField(_showConfTypeBulletsFieldName, {'fieldtype': 'Boolean'}) typeFormat.addField(_showConfTypeTableFieldName, {'fieldtype': 'Boolean'}) typeFormat.addField(_showConfTypeChildFieldName) typeFormat.addField(_showConfTypeIconFieldName) typeFormat.addField(_showConfTypeGenericFieldName) typeFormat.addField(_showConfTypeConditionFieldName) typeFormat.addField(_showConfTypeSeparatorFieldName) typeFormat.addField(_showConfTypeChildLimitFieldName) structure.treeFormats[typeFormat.name] = typeFormat fieldFormat = nodeformat.NodeFormat(_showConfFieldTypeName, structure.treeFormats, addDefaultField=True) fieldFormat.addField(_showConfFieldTypeFieldName) fieldFormat.addField(_showConfFieldFormatFieldName) fieldFormat.addField(_showConfFieldPrefixFieldName) fieldFormat.addField(_showConfFieldSuffixFieldName) fieldFormat.addField(_showConfFieldInitFieldName) fieldFormat.addField(_showConfFieldLinesFieldName, {'fieldtype': 'Number'}) fieldFormat.addField(_showConfFieldSortKeyFieldName, {'fieldtype': 'Number'}) fieldFormat.addField(_showConfFieldSortDirFieldName, {'fieldtype': 'Boolean'}) fieldFormat.addField(_showConfFieldEvalHtmlFieldName, {'fieldtype': 'Boolean'}) line = '{{*{0}*}} ({{*{1}*}})'.format(nodeformat.defaultFieldName, _showConfFieldTypeFieldName) fieldFormat.changeTitleLine(line) fieldFormat.changeOutputLines([line]) structure.treeFormats[fieldFormat.name] = fieldFormat rootNode = treenode.TreeNode(rootFormat) structure.childList.append(rootNode) structure.addNodeDictRef(rootNode) rootNode.data[nodeformat.defaultFieldName] = fileName for typeName in self.typeNames(): typeNode = treenode.TreeNode(typeFormat) rootNode.childList.append(typeNode) structure.addNodeDictRef(typeNode) typeNode.data[nodeformat.defaultFieldName] = typeName titleLine = self[typeName].getTitleLine() outputList = self[typeName].getOutputLines() if self[typeName].formatHtml: titleLine = xml.sax.saxutils.escape(titleLine) outputList = [xml.sax.saxutils.escape(line) for line in outputList] outputLines = '
\n'.join(outputList) typeNode.data[_showConfTypeTitleFieldName] = titleLine typeNode.data[_showConfTypeOutputFieldName] = outputLines spaceBetween = repr(self[typeName].spaceBetween) typeNode.data[_showConfTypeSpaceFieldName] = spaceBetween formatHtml = repr(self[typeName].formatHtml) typeNode.data[_showConfTypeHtmlFieldName] = formatHtml useBullets = repr(self[typeName].useBullets) typeNode.data[_showConfTypeBulletsFieldName] = useBullets useTables = repr(self[typeName].useTables) typeNode.data[_showConfTypeTableFieldName] = useTables typeNode.data[_showConfTypeChildFieldName] = (self[typeName]. childType) typeNode.data[_showConfTypeIconFieldName] = (self[typeName]. iconName) typeNode.data[_showConfTypeGenericFieldName] = (self[typeName]. genericType) if self[typeName].conditional: condition = self[typeName].conditional.conditionStr() typeNode.data[_showConfTypeConditionFieldName] = condition separator = self[typeName].outputSeparator typeNode.data[_showConfTypeSeparatorFieldName] = separator childLimit = ','.join(sorted(list(self[typeName].childTypeLimit))) typeNode.data[_showConfTypeChildLimitFieldName] = childLimit fieldSortKeyDict = {} fieldSortSet = False for field in self[typeName].fields(): fieldSortKeyDict[field.name] = repr(field.sortKeyNum) if field.sortKeyNum != 0: fieldSortSet = True if not fieldSortSet: sortField = list(self[typeName].fields())[0] fieldSortKeyDict[sortField.name] = repr(1) for field in self[typeName].fields(): fieldNode = treenode.TreeNode(fieldFormat) typeNode.childList.append(fieldNode) structure.addNodeDictRef(fieldNode) fieldNode.data[nodeformat.defaultFieldName] = field.name fieldNode.data[_showConfFieldTypeFieldName] = field.typeName fieldNode.data[_showConfFieldFormatFieldName] = field.format fieldNode.data[_showConfFieldPrefixFieldName] = field.prefix fieldNode.data[_showConfFieldSuffixFieldName] = field.suffix fieldNode.data[_showConfFieldInitFieldName] = field.initDefault numLines = repr(field.numLines) fieldNode.data[_showConfFieldLinesFieldName] = numLines sortKeyNum = fieldSortKeyDict[field.name] fieldNode.data[_showConfFieldSortKeyFieldName] = sortKeyNum sortKeyFwd = repr(field.sortKeyForward) fieldNode.data[_showConfFieldSortDirFieldName] = sortKeyFwd evalHtml = repr(field.evalHtml) fieldNode.data[_showConfFieldEvalHtmlFieldName] = evalHtml structure.generateSpots(None) return structure TreeLine/source/titlelistview.py0000644000175000017500000002326113464702217016037 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # titlelistview.py, provides a class for the title list view # # TreeLine, an information storage program # Copyright (C) 2019, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtGui import QKeySequence, QPalette, QTextCursor from PyQt5.QtWidgets import QAction, QTextEdit import treenode import undo import globalref class TitleListView(QTextEdit): """Class override for the title list view. Sets view defaults and updates the content. """ nodeModified = pyqtSignal(treenode.TreeNode) treeModified = pyqtSignal() shortcutEntered = pyqtSignal(QKeySequence) def __init__(self, treeView, isChildView=True, parent=None): """Initialize the title list view. Arguments: treeView - the tree view, needed for the current selection model isChildView -- shows selected nodes if false, child nodes if true parent -- the parent main window """ super().__init__(parent) self.treeView = treeView self.isChildView = isChildView self.hideChildView = not globalref.genOptions['InitShowChildPane'] self.setAcceptRichText(False) self.setLineWrapMode(QTextEdit.NoWrap) self.setTabChangesFocus(True) self.setUndoRedoEnabled(False) self.treeSelectAction = QAction(_('Select in Tree'), self) self.treeSelectAction.triggered.connect(self.selectLineInTree) self.textChanged.connect(self.readChange) def updateContents(self): """Reload the view's content if the view is shown. Avoids update if view is not visible or has zero height or width. """ selSpots = self.treeView.selectionModel().selectedSpots() if self.isChildView: if len(selSpots) > 1 or self.hideChildView: self.hide() return if not selSpots: # use top node childList from tree structure selSpots = [globalref.mainControl.activeControl.structure. structSpot()] elif not selSpots: self.hide() return self.show() if not self.isVisible() or self.height() == 0 or self.width() == 0: return if self.isChildView: selSpots = selSpots[0].childSpots() self.blockSignals(True) if selSpots: self.setPlainText('\n'.join(spot.nodeRef.title(spot) for spot in selSpots)) else: self.clear() self.blockSignals(False) def readChange(self): """Update nodes after edited by user. """ textList = [' '.join(text.split()) for text in self.toPlainText(). split('\n') if text.strip()] selSpots = self.treeView.selectionModel().selectedSpots() treeStructure = globalref.mainControl.activeControl.structure if self.isChildView: if not selSpots: selSpots = [treeStructure.structSpot()] parentSpot = selSpots[0] parent = parentSpot.nodeRef selSpots = parentSpot.childSpots() if len(selSpots) == len(textList): # collect changes first to skip false clone changes changes = [(spot.nodeRef, text) for spot, text in zip(selSpots, textList) if spot.nodeRef.title(spot) != text] for node, text in changes: undoObj = undo.DataUndo(treeStructure.undoList, node, skipSame=True) if node.setTitle(text): self.nodeModified.emit(node) else: treeStructure.undoList.removeLastUndo(undoObj) elif self.isChildView and (textList or parent != treeStructure): undo.ChildDataUndo(treeStructure.undoList, parent) # clear hover to avoid crash if deleted child item was hovered over self.treeView.clearHover() isDeleting = len(selSpots) > len(textList) control = globalref.mainControl.activeControl if isDeleting and len(control.windowList) > 1: # clear other window selections that are about to be deleted for window in control.windowList: if window != control.activeWindow: selectModel = window.treeView.selectionModel() ancestors = set() for spot in selectModel.selectedBranchSpots(): ancestors.update(set(spot.spotChain())) if ancestors & set(selSpots): selectModel.selectSpots([], False) expandState = self.treeView.savedExpandState(selSpots) parent.replaceChildren(textList, treeStructure) self.treeView.restoreExpandState(expandState) if self.treeView.selectionModel().selectedSpots(): self.treeView.expandSpot(parentSpot) self.treeModified.emit() else: self.updateContents() # remove illegal changes def selectLineInTree(self): """Select the node for the current line in the tree view. """ selSpots = self.treeView.selectionModel().selectedSpots() if not selSpots: selSpots = [treeStructure.structSpot()] spotList = selSpots[0].childSpots() cursor = self.textCursor() blockStart = cursor.block().position() # check for selection all on one line if (cursor.selectionStart() >= blockStart and cursor.selectionEnd() < blockStart + cursor.block().length()): lineNum = cursor.blockNumber() if len(spotList) > lineNum: self.treeView.selectionModel().selectSpots([spotList[lineNum]], True, True) def hasSelectedText(self): """Return True if text is selected. """ return self.textCursor().hasSelection() def highlightSearch(self, wordList=None, regExpList=None): """Highlight any found search terms. Arguments: wordList -- list of words to highlight regExpList -- a list of regular expression objects to highlight """ backColor = self.palette().brush(QPalette.Active, QPalette.Highlight) foreColor = self.palette().brush(QPalette.Active, QPalette.HighlightedText) if wordList is None: wordList = [] if regExpList is None: regExpList = [] for regExp in regExpList: for match in regExp.finditer(self.toPlainText()): matchText = match.group() if matchText not in wordList: wordList.append(matchText) selections = [] for word in wordList: while self.find(word): extraSel = QTextEdit.ExtraSelection() extraSel.cursor = self.textCursor() extraSel.format.setBackground(backColor) extraSel.format.setForeground(foreColor) selections.append(extraSel) cursor = QTextCursor(self.document()) self.setTextCursor(cursor) # reset main cursor/selection self.setExtraSelections(selections) def focusInEvent(self, event): """Handle focus-in to put cursor at end for tab-based focus. Arguments: event -- the focus in event """ if event.reason() in (Qt.TabFocusReason, Qt.BacktabFocusReason): self.moveCursor(QTextCursor.End) super().focusInEvent(event) def contextMenuEvent(self, event): """Override popup menu to remove local undo. Arguments: event -- the menu event """ menu = self.createStandardContextMenu() menu.removeAction(menu.actions()[0]) menu.removeAction(menu.actions()[0]) menu.insertSeparator(menu.actions()[0]) menu.insertAction(menu.actions()[0], self.treeSelectAction) self.treeSelectAction.setEnabled(self.isChildView and len(self.toPlainText().strip()) > 0) menu.exec_(event.globalPos()) def keyPressEvent(self, event): """Customize handling of return and control keys. Ignore return key if not in show children mode and emit a signal for app to handle control keys. Arguments: event -- the key press event """ if (event.modifiers() == Qt.ControlModifier and Qt.Key_A <= event.key() <= Qt.Key_Z): key = QKeySequence(event.modifiers() | event.key()) self.shortcutEntered.emit(key) return if self.isChildView or event.key() not in (Qt.Key_Enter, Qt.Key_Return): super().keyPressEvent(event) def resizeEvent(self, event): """Update view if it was collaped by splitter. """ if ((event.oldSize().height() == 0 and event.size().height()) or (event.oldSize().width() == 0 and event.size().width())): self.updateContents() return super().resizeEvent(event) TreeLine/source/nodeformat.py0000644000175000017500000006056713530024673015274 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # nodeformat.py, provides a class to handle node format objects # # TreeLine, an information storage program # Copyright (C) 2019, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import collections import os.path import sys import copy import operator import datetime import xml.sax.saxutils if not sys.platform.startswith('win'): import pwd import fieldformat import conditional defaultFieldName = _('Name') _defaultOutputSeparator = ', ' _fieldSplitRe = re.compile(r'({\*(?:\**|\?|!|&|#)[\w_\-.]+\*})') _fieldPartRe = re.compile(r'{\*(\**|\?|!|&|#)([\w_\-.]+)\*}') _endTagRe = re.compile(r'.*(|||)$') _levelFieldRe = re.compile(r'[^0-9]+([0-9]+)$') class NodeFormat: """Class to handle node format info Stores node field lists and line formatting. Provides methods to return formatted data. """ def __init__(self, name, parentFormats, formatData=None, addDefaultField=False): """Initialize a tree format. Arguments: name -- the type name string parentFormats -- a ref to TreeFormats class for outside field refs formatData -- the JSON dict for this format addDefaultField -- if true, adds a default initial field """ self.name = name self.parentFormats = parentFormats self.savedConditionText = {} self.conditional = None self.childTypeLimit = set() self.readFormat(formatData) self.siblingPrefix = '' self.siblingSuffix = '' self.derivedTypes = [] self.origOutputLines = [] # lines without bullet or table modifications self.sortFields = [] # temporary storage while sorting if addDefaultField: self.addFieldIfNew(defaultFieldName) self.titleLine = ['{{*{0}*}}'.format(defaultFieldName)] self.outputLines = [['{{*{0}*}}'.format(defaultFieldName)]] self.updateLineParsing() if self.useBullets: self.addBullets() if self.useTables: self.addTables() def readFormat(self, formatData=None): """Read JSON format data into this format. Arguments: formatData -- JSON dict for this format (None for default settings) """ self.fieldDict = collections.OrderedDict() if formatData: for fieldData in formatData.get('fields', []): fieldName = fieldData['fieldname'] self.addField(fieldName, fieldData) else: formatData = {} self.titleLine = [formatData.get('titleline', '')] self.outputLines = [[line] for line in formatData.get('outputlines', [])] self.spaceBetween = formatData.get('spacebetween', True) self.formatHtml = formatData.get('formathtml', False) self.useBullets = formatData.get('bullets', False) self.useTables = formatData.get('tables', False) self.childType = formatData.get('childtype', '') self.genericType = formatData.get('generic', '') if 'condition' in formatData: self.conditional = conditional.Conditional(formatData['condition']) if 'childTypeLimit' in formatData: self.childTypeLimit = set(formatData['childTypeLimit']) self.iconName = formatData.get('icon', '') self.outputSeparator = formatData.get('outputsep', _defaultOutputSeparator) for key in formatData.keys(): if key.startswith('cond-'): self.savedConditionText[key[5:]] = formatData[key] def storeFormat(self): """Return JSON format data for this format. """ formatData = {} formatData['formatname'] = self.name formatData['fields'] = [field.formatData() for field in self.fields()] formatData['titleline'] = self.getTitleLine() formatData['outputlines'] = self.getOutputLines() if not self.spaceBetween: formatData['spacebetween'] = False if self.formatHtml: formatData['formathtml'] = True if self.useBullets: formatData['bullets'] = True if self.useTables: formatData['tables'] = True if self.childType: formatData['childtype'] = self.childType if self.genericType: formatData['generic'] = self.genericType if self.conditional: formatData['condition'] = self.conditional.conditionStr() if self.childTypeLimit: formatData['childTypeLimit'] = sorted(list(self.childTypeLimit)) if self.iconName: formatData['icon'] = self.iconName if self.outputSeparator != _defaultOutputSeparator: formatData['outputsep'] = self.outputSeparator for key, text in self.savedConditionText.items(): formatData['cond-' + key] = text return formatData def copySettings(self, sourceFormat): """Copy all settings from another format to this one. Arguments: sourceFormat -- the format to copy """ self.name = sourceFormat.name self.readFormat(sourceFormat.storeFormat()) self.siblingPrefix = sourceFormat.siblingPrefix self.siblingSuffix = sourceFormat.siblingSuffix self.outputLines = sourceFormat.getOutputLines(False) self.origOutputLines = sourceFormat.getOutputLines() self.updateLineParsing() def fields(self): """Return list of all fields. """ return self.fieldDict.values() def fieldNames(self): """Return list of names of all fields. """ return list(self.fieldDict.keys()) def formatTitle(self, node, spotRef=None): """Return a string with formatted title data. Arguments: node -- the node used to get data for fields spotRef -- optional, used for ancestor field refs """ line = ''.join([part.outputText(node, True, True, self.formatHtml) if hasattr(part, 'outputText') else part for part in self.titleLine]) return line.strip() def formatOutput(self, node, plainText=False, keepBlanks=False, spotRef=None): """Return a list of formatted text output lines. Arguments: node -- the node used to get data for fields plainText -- if True, remove HTML markup from fields and formats keepBlanks -- if True, keep lines with empty fields spotRef -- optional, used for ancestor field refs """ result = [] for lineData in self.outputLines: line = '' numEmptyFields = 0 numFullFields = 0 for part in lineData: if hasattr(part, 'outputText'): text = part.outputText(node, False, plainText, self.formatHtml) if text: numFullFields += 1 else: numEmptyFields += 1 line += text else: if not self.formatHtml and not plainText: part = xml.sax.saxutils.escape(part) elif self.formatHtml and plainText: part = fieldformat.removeMarkup(part) line += part if keepBlanks or numFullFields or not numEmptyFields: result.append(line) elif self.formatHtml and not plainText and result: # add ending HTML tag from skipped line back to previous line endTagMatch = _endTagRe.match(line) if endTagMatch: result[-1] += endTagMatch.group(1) return result def addField(self, name, fieldData=None): """Add a field type with its format to the field list. Arguments: name -- the field name string fieldData -- the dict that defines this field's format """ if not fieldData: fieldData = {} typeName = '{}Field'.format(fieldData.get('fieldtype', 'Text')) fieldClass = getattr(fieldformat, typeName, fieldformat.TextField) field = fieldClass(name, fieldData) self.fieldDict[name] = field def addFieldIfNew(self, name, fieldData=None): """Add a field type to the field list if not already there. Arguments: name -- the field name string fieldData -- the dict that defines this field's format """ if name not in self.fieldDict: self.addField(name, fieldData) def addFieldList(self, nameList, addFirstTitle=False, addToOutput=False): """Add text fields with names given in list. Also add to title and output lines if addOutput is True. Arguments: nameList -- the list of names to add addFirstTitle -- if True, use first field for title output format addToOutput -- replace output lines with all fields if True """ for name in nameList: self.addFieldIfNew(name) if addFirstTitle: self.changeTitleLine('{{*{0}*}}'.format(nameList[0])) if addToOutput: self.changeOutputLines(['{{*{0}*}}'.format(name) for name in nameList]) def reorderFields(self, fieldNameList): """Change the order of fieldDict to match the given list. Arguments: fieldNameList -- a list of existing field names in a desired order """ newFieldDict = collections.OrderedDict() for fieldName in fieldNameList: newFieldDict[fieldName] = self.fieldDict[fieldName] self.fieldDict = newFieldDict def removeField(self, field): """Remove all occurances of field from title and output lines. Arguments: field -- the field to be removed """ while field in self.titleLine: self.titleLine.remove(field) for lineData in self.outputLines: while field in lineData: lineData.remove(field) self.outputLines = [line for line in self.outputLines if line] # if len(self.lineList) == 0: # self.lineList.append(['']) def setInitDefaultData(self, data, overwrite=False): """Add initial default data from fields into supplied data dict. Arguments: data -- the data dict to modify overwrite -- if true, replace previous data entries """ for field in self.fields(): text = field.getInitDefault() if text and (overwrite or not data.get(field.name, '')): data[field.name] = text def updateLineParsing(self): """Update the fields parsed in the output lines. Converts lines back to whole lines with embedded field names, then parse back to individual fields and text. """ self.titleLine = self.parseLine(self.getTitleLine()) self.outputLines = [self.parseLine(line) for line in self.getOutputLines(False)] if self.origOutputLines: self.origOutputLines = [self.parseLine(line) for line in self.getOutputLines(True)] def parseLine(self, text): """Parse text format line, return list of field types and text. Splits the line into field and text segments. Arguments: text -- the raw format text line to be parsed """ text = ' '.join(text.split()) segments = (part for part in _fieldSplitRe.split(text) if part) return [self.parseField(part) for part in segments] def parseField(self, text): """Parse text field, return field type or plain text if not a field. Arguments: text -- the raw format text (could be a field) """ fieldMatch = _fieldPartRe.match(text) if fieldMatch: modifier = fieldMatch.group(1) fieldName = fieldMatch.group(2) try: if not modifier: return self.fieldDict[fieldName] elif modifier == '*' * len(modifier): return fieldformat.AncestorLevelField(fieldName, len(modifier)) elif modifier == '?': return fieldformat.AnyAncestorField(fieldName) elif modifier == '&': return fieldformat.ChildListField(fieldName) elif modifier == '#': match = _levelFieldRe.match(fieldName) if match and match.group(1) != '0': level = int(match.group(1)) return fieldformat.DescendantCountField(fieldName, level) elif modifier == '!': return (self.parentFormats.fileInfoFormat. fieldDict[fieldName]) except KeyError: pass return text def getTitleLine(self): """Return text of title format with field names embedded. """ return ''.join([part.sepName() if hasattr(part, 'sepName') else part for part in self.titleLine]) def getOutputLines(self, useOriginal=True): """Return text list of output format lines with field names embedded. Arguments: useOriginal -- use original line list, wothout bullet or table mods """ lines = self.outputLines if useOriginal and self.origOutputLines: lines = self.origOutputLines lines = [''.join([part.sepName() if hasattr(part, 'sepName') else part for part in line]) for line in lines] return lines if lines else [''] def changeTitleLine(self, text): """Replace the title format line. Arguments: text -- the new title format line """ self.titleLine = self.parseLine(text) if not self.titleLine: self.titleLine = [''] def changeOutputLines(self, lines, keepBlanks=False): """Replace the output format lines with given list. Arguments: lines -- a list of replacement format lines keepBlanks -- if False, ignore blank lines """ self.outputLines = [] for line in lines: newLine = self.parseLine(line) if keepBlanks or newLine: self.outputLines.append(newLine) if self.useBullets: self.origOutputLines = self.outputLines[:] self.addBullets() if self.useTables: self.origOutputLines = self.outputLines[:] self.addTables() def addOutputLine(self, line): """Add an output format line after existing lines. Arguments: line -- the text line to add """ newLine = self.parseLine(line) if newLine: self.outputLines.append(newLine) def extractTitleData(self, titleString, data): """Modifies the data dictionary based on a title string. Match the title format to the string, return True if successful. Arguments: title -- the string with the new title data -- the data dictionary to be modified """ fields = [] pattern = '' extraText = '' for seg in self.titleLine: if hasattr(seg, 'name'): # a field segment fields.append(seg) pattern += '(.*)' else: # a text separator pattern += re.escape(seg) extraText += seg match = re.match(pattern, titleString) try: if match: for num, field in enumerate(fields): text = match.group(num + 1) data[field.name] = field.storedTextFromTitle(text) elif not extraText.strip(): # assign to 1st field if sep is only spaces text = fields[0].storedTextFromTitle(titleString) data[fields[0].name] = text for field in fields[1:]: data[field.name] = '' else: return False except ValueError: return False return True def updateDerivedTypes(self): """Update derived types after changes to this generic type. """ for derivedType in self.derivedTypes: derivedType.updateFromGeneric(self) def updateFromGeneric(self, genericType=None, formatsRef=None): """Update fields and field types to match a generic type. Does nothing if self is not a derived type. Must provide either the genericType or a formatsRef. Arguments: genericType -- the type to update from formatsRef -- the tree formats dict to update from """ if not self.genericType: return if not genericType: genericType = formatsRef[self.genericType] newFields = collections.OrderedDict() for field in genericType.fieldDict.values(): fieldMatch = self.fieldDict.get(field.name, None) if fieldMatch and field.typeName == fieldMatch.typeName: newFields[field.name] = fieldMatch else: newFields[field.name] = copy.deepcopy(field) self.fieldDict = newFields self.updateLineParsing() def addBullets(self): """Add bullet HTML tags to sibling prefix, suffix and output lines. """ self.siblingPrefix = '
    ' self.siblingSuffix = '
' lines = self.getOutputLines() if lines != ['']: lines[0] = '
  • ' + lines[0] lines[-1] += '
  • ' self.origOutputLines = self.outputLines[:] self.outputLines = lines self.updateLineParsing() def addTables(self): """Add table HTML tags to sibling prefix, suffix and output lines. """ lines = [line for line in self.getOutputLines() if line] newLines = [] headings = [] for line in lines: head = '' firstPart = self.parseLine(line)[0] if hasattr(firstPart, 'split') and ':' in firstPart: head, line = line.split(':', 1) newLines.append(line.strip()) headings.append(head.strip()) self.siblingPrefix = '' if [head for head in headings if head]: self.siblingPrefix += '' for head in headings: self.siblingPrefix = ('{0}'. format(self.siblingPrefix, head)) self.siblingPrefix += '' self.siblingSuffix = '
    {1}
    ' newLines = ['{0}'.format(line) for line in newLines] newLines[0] = '' + newLines[0] newLines[-1] += '' self.origOutputLines = self.outputLines[:] self.outputLines = newLines self.updateLineParsing() def clearBulletsAndTables(self): """Remove any HTML tags for bullets and tables. """ self.siblingPrefix = '' self.siblingSuffix = '' if self.origOutputLines: self.outputLines = self.origOutputLines self.updateLineParsing() self.origOutputLines = [] def numberingFieldList(self): """Return a list of numbering field names. """ return [field.name for field in self.fieldDict.values() if field.typeName == 'Numbering'] def loadSortFields(self): """Add sort fields to temporarily stored list. Only used for efficiency while sorting. """ self.sortFields = [field for field in self.fields() if field.sortKeyNum > 0] self.sortFields.sort(key = operator.attrgetter('sortKeyNum')) if not self.sortFields: self.sortFields = [list(self.fields())[0]] class FileInfoFormat(NodeFormat): """Node format class to store and update special file info fields. Fields used in print header/footer and in outputs of other node types. """ typeName = 'INT_TL_FILE_DATA_FORM' fileFieldName = 'File_Name' pathFieldName = 'File_Path' sizeFieldName = 'File_Size' dateFieldName = 'File_Mod_Date' timeFieldName = 'File_Mod_Time' ownerFieldName = 'File_Owner' pageNumFieldName = 'Page_Number' numPagesFieldName = 'Number_of_Pages' def __init__(self, parentFormats): """Create a file info format. """ super().__init__(FileInfoFormat.typeName, parentFormats) self.fieldFormatModified = False self.addField(FileInfoFormat.fileFieldName) self.addField(FileInfoFormat.pathFieldName) self.addField(FileInfoFormat.sizeFieldName, {'fieldtype': 'Number'}) self.addField(FileInfoFormat.dateFieldName, {'fieldtype': 'Date'}) self.addField(FileInfoFormat.timeFieldName, {'fieldtype': 'Time'}) if not sys.platform.startswith('win'): self.addField(FileInfoFormat.ownerFieldName) # page info only for print header: self.addField(FileInfoFormat.pageNumFieldName) self.fieldDict[FileInfoFormat.pageNumFieldName].showInDialog = False self.addField(FileInfoFormat.numPagesFieldName) self.fieldDict[FileInfoFormat.numPagesFieldName].showInDialog = False for field in self.fields(): field.useFileInfo = True def updateFileInfo(self, fileObj, fileInfoNode): """Update data of file info node. Arguments: fileObj -- the TreeLine file path object fileInfoNode -- the node to update """ try: status = fileObj.stat() except (AttributeError, OSError): fileInfoNode.data = {} return fileInfoNode.data[FileInfoFormat.fileFieldName] = fileObj.name fileInfoNode.data[FileInfoFormat.pathFieldName] = fileObj.parent fileInfoNode.data[FileInfoFormat.sizeFieldName] = str(status.st_size) modDateTime = datetime.datetime.fromtimestamp(status.st_mtime) modDate = modDateTime.date().strftime(fieldformat.DateField.isoFormat) modTime = modDateTime.time().strftime(fieldformat.TimeField.isoFormat) fileInfoNode.data[FileInfoFormat.dateFieldName] = modDate fileInfoNode.data[FileInfoFormat.timeFieldName] = modTime if not sys.platform.startswith('win'): try: owner = pwd.getpwuid(status.st_uid)[0] except KeyError: owner = repr(status.st_uid) fileInfoNode.data[FileInfoFormat.ownerFieldName] = owner def duplicateFileInfo(self, altFileFormat): """Copy field format settings from alternate file format. Arguments: altFileFormat -- the file info format to copy from """ for field in self.fields(): altField = altFileFormat.fieldDict.get(field.name) if altField: if field.format != altField.format: field.setFormat(altField.format) self.fieldFormatModified = True if altField.prefix: field.prefix = altField.prefix self.fieldFormatModified = True if altField.suffix: field.suffix = altField.suffix self.fieldFormatModified = True class DescendantCountFormat(NodeFormat): """Placeholder format for child count fields. Should not show up in main format type list. """ countFieldName = 'Level' def __init__(self): super().__init__('CountFormat', None) for level in range(3): name = '{0}{1}'.format(DescendantCountFormat.countFieldName, level + 1) field = fieldformat.DescendantCountField(name, level + 1) self.fieldDict[name] = field TreeLine/source/undo.py0000644000175000017500000004263013363127530014072 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # undo.py, provides a classes to store and execute undo & redo operations # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import copy import treenode import globalref class UndoRedoList(list): """Stores undo or redo objects. """ def __init__(self, action, localControlRef): """Initialize the undo or redo storage. Set the number of stored levels based on the user option. Arguments: action -- the Qt action for undo/redo menus localControlRef -- ref control class for selections, modified, etc. """ super().__init__() self.action = action self.action.setEnabled(False) self.localControlRef = localControlRef self.levels = globalref.genOptions['UndoLevels'] self.altListRef = None # holds a ref to redo or undo list def addUndoObj(self, undoObject, clearRedo=True): """Add the given undo or redo object to the list. Arguments: undoObject -- the object to be added clearRedo -- if true, clear redo list (can't redo after changes) """ self.append(undoObject) del self[:-self.levels] if self.levels == 0: del self[:] self.action.setEnabled(len(self) > 0) if clearRedo and self.altListRef: self.altListRef.clearList() def clearList(self): """Empty the undo/redo list, primarily for no redo after a change. """ del self[:] self.action.setEnabled(False) def setNumLevels(self): """Change number of stored undo levels to the stored option. """ self.levels = globalref.genOptions['UndoLevels'] del self[:-self.levels] if self.levels == 0: del self[:] self.action.setEnabled(len(self) > 0) def removeLastUndo(self, undoObject): """Remove the last undo object if it matches the given object. Arguments: undoObject -- the object to be removed """ if self[-1] is undoObject: del self[-1] self.action.setEnabled(len(self) > 0) def undo(self): """Save current state to altListRef and restore the last saved state. Remove the last undo item from the list. Restore the previous selection and saved doc modified state. """ # # clear selection to avoid crash due to invalid selection: # self.localControlRef.currentSelectionModel().selectSpots([], False) item = self.pop() item.undo(self.altListRef) selectSpots = [node.spotByNumber(num) for (node, num) in item.selectedTuples] self.localControlRef.currentSelectionModel().selectSpots(selectSpots, False) self.localControlRef.setModified(item.modified) self.action.setEnabled(len(self) > 0) class UndoBase: """Abstract base class for undo objects. """ def __init__(self, localControlRef): """Initialize data storage, selected nodes and doc modified status. Arguments: localControlRef -- ref control class for selections, modified, etc. """ self.dataList = [] self.treeStructRef = localControlRef.structure self.selectedTuples = [(spot.nodeRef, spot.instanceNumber()) for spot in localControlRef.currentSelectionModel(). selectedSpots()] self.modified = localControlRef.modified class DataUndo(UndoBase): """Info for undo/redo of tree node data changes. """ def __init__(self, listRef, nodes, addChildren=False, addBranch=False, skipSame=False, fieldRef='', notRedo=True): """Create the data undo class and add it to the undoStore. Can't use skipSame if addChildren or addBranch are True. Arguments: listRef -- a ref to the undo/redo list this gets added to nodes -- a node or a list of nodes to back up addChildren -- if True, include child nodes addBranch -- if True, include all branch nodes (ignores addChildren skipSame -- if true, don't add an undo that is similar to the last fieldRef -- optional field name ref to check for similar changes notRedo -- if True, clear redo list (after changes) """ super().__init__(listRef.localControlRef) if not isinstance(nodes, list): nodes = [nodes] if (skipSame and listRef and isinstance(listRef[-1], DataUndo) and len(listRef[-1].dataList) == 1 and len(nodes) == 1 and nodes[0] == listRef[-1].dataList[0][0] and fieldRef == listRef[-1].dataList[0][2]): return for node in nodes: if addBranch: for child in node.descendantGen(): self.dataList.append((child, child.data.copy(), '')) else: self.dataList.append((node, node.data.copy(), fieldRef)) if addChildren: for child in node.childList: self.dataList.append((child, child.data.copy(), '')) listRef.addUndoObj(self, notRedo) def undo(self, redoRef): """Save current state to redoRef and restore saved state. Arguments: redoRef -- the redo list where the current state is saved """ if redoRef != None: DataUndo(redoRef, [data[0] for data in self.dataList], False, False, False, '', False) for node, data, fieldRef in self.dataList: node.data = data class ChildListUndo(UndoBase): """Info for undo/redo of tree node child lists. """ def __init__(self, listRef, nodes, addBranch=False, treeFormats=None, skipSame=False, notRedo=True): """Create the child list undo class and add it to the undoStore. Also stores data formats if given. Can't use skipSame if addBranch is True. Arguments: listRef -- a ref to the undo/redo list this gets added to nodes -- a parent node or a list of parents to save children addBranch -- if True, include all branch nodes treeFormats -- the format data to store skipSame -- if true, don't add an undo that is similar to the last notRedo -- if True, clear redo list (after changes) """ super().__init__(listRef.localControlRef) if not isinstance(nodes, list): nodes = [nodes] if (skipSame and listRef and isinstance(listRef[-1], ChildListUndo) and len(listRef[-1].dataList) == 1 and len(nodes) == 1 and nodes[0] == listRef[-1].dataList[0][0]): return self.addBranch = addBranch self.treeFormats = None if treeFormats: self.treeFormats = copy.deepcopy(treeFormats) for node in nodes: if addBranch: for child in node.descendantGen(): self.dataList.append((child, child.childList[:])) else: self.dataList.append((node, node.childList[:])) listRef.addUndoObj(self, notRedo) def undo(self, redoRef): """Save current state to redoRef and restore saved state. Arguments: redoRef -- the redo list where the current state is saved """ if redoRef != None: formats = None if self.treeFormats: formats = self.treeStructRef.treeFormats ChildListUndo(redoRef, [data[0] for data in self.dataList], False, formats, False, False) if self.treeFormats: self.treeStructRef.configDialogFormats = self.treeFormats self.treeStructRef.applyConfigDialogFormats(False) globalref.mainControl.updateConfigDialog() newNodes = set() oldNodes = set() for node, childList in self.dataList: origChildren = set(node.childList) children = set(childList) newNodes = newNodes | (children - origChildren) oldNodes = oldNodes | (origChildren - children) for node, childList in self.dataList: node.childList = childList self.treeStructRef.rebuildNodeDict() # slow but reliable for oldNode in oldNodes: oldNode.removeInvalidSpotRefs() for node, childList in self.dataList: for child in childList: if child in newNodes: child.addSpotRef(node) class ChildDataUndo(UndoBase): """Info for undo/redo of tree node child data and lists. """ def __init__(self, listRef, nodes, addBranch=False, treeFormats=None, notRedo=True): """Create the child data undo class and add it to the undoStore. Arguments: listRef -- a ref to the undo/redo list this gets added to nodes -- a parent node or a list of parents to save children addBranch -- if True, include all branch nodes treeFormats -- the format data to store notRedo -- if True, clear redo list (after changes) """ super().__init__(listRef.localControlRef) if not isinstance(nodes, list): nodes = [nodes] self.addBranch = addBranch self.treeFormats = None if treeFormats: self.treeFormats = copy.deepcopy(treeFormats) for parent in nodes: if addBranch: for node in parent.descendantGen(): self.dataList.append((node, node.data.copy(), node.childList[:])) else: self.dataList.append((parent, parent.data.copy(), parent.childList[:])) for node in parent.childList: self.dataList.append((node, node.data.copy(), node.childList[:])) listRef.addUndoObj(self, notRedo) def undo(self, redoRef): """Save current state to redoRef and restore saved state. Arguments: redoRef -- the redo list where the current state is saved """ if redoRef != None: formats = None if self.treeFormats: formats = self.treeStructRef.treeFormats ChildDataUndo(redoRef, [data[0] for data in self.dataList], False, formats, False) if self.treeFormats: self.treeStructRef.configDialogFormats = self.treeFormats self.treeStructRef.applyConfigDialogFormats(False) globalref.mainControl.updateConfigDialog() newNodes = set() oldNodes = set() for node, data, childList in self.dataList: origChildren = set(node.childList) children = set(childList) newNodes = newNodes | (children - origChildren) oldNodes = oldNodes | (origChildren - children) for node, data, childList in self.dataList: node.childList = childList node.data = data self.treeStructRef.rebuildNodeDict() # slow but reliable for newNode in newNodes.copy(): for child in newNode.descendantGen(): newNodes.add(child) for oldNode in oldNodes: oldNode.removeInvalidSpotRefs() for node, data, childList in self.dataList: for child in childList: if child in newNodes: child.addSpotRef(node, not self.addBranch) class TypeUndo(UndoBase): """Info for undo/redo of tree node type name changes. Also saves node data to cover blank node title replacement and initial data settings. """ def __init__(self, listRef, nodes, notRedo=True): """Create the data undo class and add it to the undoStore. Arguments: listRef -- a ref to the undo/redo list this gets added to nodes -- a node or a list of nodes to back up notRedo -- if True, add clones and clear redo list (after changes) """ super().__init__(listRef.localControlRef) if not isinstance(nodes, list): nodes = [nodes] for node in nodes: self.dataList.append((node, node.formatRef.name, node.data.copy())) listRef.addUndoObj(self, notRedo) def undo(self, redoRef): """Save current state to redoRef and restore saved state. Arguments: redoRef -- the redo list where the current state is saved """ if redoRef != None: TypeUndo(redoRef, [data[0] for data in self.dataList], False) for node, formatName, data in self.dataList: node.formatRef = self.treeStructRef.treeFormats[formatName] node.data = data class FormatUndo(UndoBase): """Info for undo/redo of tree node type format changes. """ def __init__(self, listRef, origTreeFormats, newTreeFormats, notRedo=True): """Create the data undo class and add it to the undoStore. Arguments: listRef -- a ref to the undo/redo list this gets added to origTreeFormats -- the format data to store newTreeFormats -- the replacement format, contains rename dicts notRedo -- if True, clear redo list (after changes) """ super().__init__(listRef.localControlRef) self.treeFormats = copy.deepcopy(origTreeFormats) self.treeFormats.fieldRenameDict = {} for typeName, fieldDict in newTreeFormats.fieldRenameDict.items(): self.treeFormats.fieldRenameDict[typeName] = {} for oldName, newName in fieldDict.items(): self.treeFormats.fieldRenameDict[typeName][newName] = oldName self.treeFormats.typeRenameDict = {} for oldName, newName in newTreeFormats.typeRenameDict.items(): self.treeFormats.typeRenameDict[newName] = oldName if newName in self.treeFormats.fieldRenameDict: self.treeFormats.fieldRenameDict[oldName] = (self.treeFormats. fieldRenameDict[newName]) del self.treeFormats.fieldRenameDict[newName] listRef.addUndoObj(self, notRedo) def undo(self, redoRef): """Save current state to redoRef and restore saved state. Arguments: redoRef -- the redo list where the current state is saved """ if redoRef != None: FormatUndo(redoRef, self.treeStructRef.treeFormats, self.treeFormats, False) self.treeStructRef.configDialogFormats = self.treeFormats self.treeStructRef.applyConfigDialogFormats(False) globalref.mainControl.updateConfigDialog() class ParamUndo(UndoBase): """Info for undo/redo of any variable parameter. """ def __init__(self, listRef, varList, notRedo=True): """Create the data undo class and add it to the undoStore. Arguments: listRef -- a ref to the undo/redo list this gets added to varList - list of tuples, variable's owner and variable's name notRedo -- if True, clear redo list (after changes) """ super().__init__(listRef.localControlRef) for varOwner, varName in varList: value = varOwner.__dict__[varName] self.dataList.append((varOwner, varName, value)) listRef.addUndoObj(self, notRedo) def undo(self, redoRef): """Save current state to redoRef and restore saved state. Arguments: redoRef -- the redo list where the current state is saved """ if redoRef != None: ParamUndo(redoRef, [item[:2] for item in self.dataList], False) for varOwner, varName, value in self.dataList: varOwner.__dict__[varName] = value class StateSettingUndo(UndoBase): """Info for undo/redo of objects with get/set functions for attributes. """ def __init__(self, listRef, getFunction, setFunction, notRedo=True): """Create the data undo class and add it to the undoStore. Arguments: listRef -- a ref to the undo/redo list this gets added to getFunction -- a function ref that returns a state variable setFunction -- a function ref that restores from the state varible notRedo -- if True, clear redo list (after changes) """ super().__init__(listRef.localControlRef) self.getFunction = getFunction self.setFunction = setFunction self.data = getFunction() listRef.addUndoObj(self, notRedo) def undo(self, redoRef): """Save current state to redoRef and restore saved state. Arguments: redoRef -- the redo list where the current state is saved """ if redoRef != None: StateSettingUndo(redoRef, self.getFunction, self.setFunction, False) self.setFunction(self.data) TreeLine/source/helpview.py0000644000175000017500000001252513363127527014756 0ustar dougdoug#!/usr/bin/env python3 #**************************************************************************** # helpview.py, provides a window for viewing an html help file # # Copyright (C) 2017, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QTextDocument from PyQt5.QtWidgets import (QAction, QLabel, QLineEdit, QMainWindow, QMenu, QStatusBar, QTextBrowser) import dataeditors class HelpView(QMainWindow): """Main window for viewing an html help file. """ def __init__(self, pathObj, caption, icons, parent=None): """Helpview initialize with text. Arguments: pathObj -- a path object for the help file caption -- the window caption icons -- dict of view icons """ QMainWindow.__init__(self, parent) self.setAttribute(Qt.WA_QuitOnClose, False) self.setWindowFlags(Qt.Window) self.setStatusBar(QStatusBar()) self.textView = HelpViewer(self) self.setCentralWidget(self.textView) self.textView.setSearchPaths([str(pathObj.parent)]) self.textView.setSource(QUrl(pathObj.as_uri())) self.resize(520, 440) self.setWindowTitle(caption) tools = self.addToolBar(_('Tools')) self.menu = QMenu(self.textView) self.textView.highlighted[str].connect(self.showLink) backAct = QAction(_('&Back'), self) backAct.setIcon(icons['helpback']) tools.addAction(backAct) self.menu.addAction(backAct) backAct.triggered.connect(self.textView.backward) backAct.setEnabled(False) self.textView.backwardAvailable.connect(backAct.setEnabled) forwardAct = QAction(_('&Forward'), self) forwardAct.setIcon(icons['helpforward']) tools.addAction(forwardAct) self.menu.addAction(forwardAct) forwardAct.triggered.connect(self.textView.forward) forwardAct.setEnabled(False) self.textView.forwardAvailable.connect(forwardAct.setEnabled) homeAct = QAction(_('&Home'), self) homeAct.setIcon(icons['helphome']) tools.addAction(homeAct) self.menu.addAction(homeAct) homeAct.triggered.connect(self.textView.home) tools.addSeparator() tools.addSeparator() findLabel = QLabel(_(' Find: '), self) tools.addWidget(findLabel) self.findEdit = QLineEdit(self) tools.addWidget(self.findEdit) self.findEdit.textEdited.connect(self.findTextChanged) self.findEdit.returnPressed.connect(self.findNext) self.findPreviousAct = QAction(_('Find &Previous'), self) self.findPreviousAct.setIcon(icons['helpprevious']) tools.addAction(self.findPreviousAct) self.menu.addAction(self.findPreviousAct) self.findPreviousAct.triggered.connect(self.findPrevious) self.findPreviousAct.setEnabled(False) self.findNextAct = QAction(_('Find &Next'), self) self.findNextAct.setIcon(icons['helpnext']) tools.addAction(self.findNextAct) self.menu.addAction(self.findNextAct) self.findNextAct.triggered.connect(self.findNext) self.findNextAct.setEnabled(False) def showLink(self, text): """Send link text to the statusbar. Arguments: text -- link text to show """ self.statusBar().showMessage(text) def findTextChanged(self, text): """Update find controls based on text in text edit. Arguments: text -- the search 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(self.findEdit.text(), 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(self.findEdit.text()): self.statusBar().clearMessage() else: self.statusBar().showMessage(_('Text string not found')) class HelpViewer(QTextBrowser): """Shows an html help file. """ def __init__(self, parent=None): """Initialize the viewer. Arguments: parent -- the parent widget, if given """ QTextBrowser.__init__(self, parent) def setSource(self, url): """Called when user clicks on a URL. Arguments: url -- the clicked on QUrl """ name = url.toString() if name.startswith('http'): dataeditors.openExtUrl(name) else: QTextBrowser.setSource(self, QUrl(name)) def contextMenuEvent(self, event): """Init popup menu on right click"". Arguments: event -- the menu event """ self.parentWidget().menu.exec_(event.globalPos()) TreeLine/source/setup.py0000644000175000017500000000356113363127527014273 0ustar dougdoug#****************************************************************************** # setup.py, provides a distutils script for use with cx_Freeze # # Creates a standalone windows executable # # Run the build process by running the command 'python setup.py build' # # If everything works well you should find a subdirectory in the build # subdirectory that contains the files needed to run the application # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import sys from cx_Freeze import setup, Executable from treeline import __version__ base = None if sys.platform == 'win32': base = 'Win32GUI' extraFiles = [('../doc', 'doc'), ('../icons', 'icons'), ('../samples', 'samples'), ('../source', 'source'), ('../templates', 'templates'), ('../translations', 'translations'), ('../win', '.')] setup(name = 'treeline', version = __version__, description = 'TreeLine info storage program', options = {'build_exe': {'includes': ['atexit', 'PyQt5.sip'], 'include_files': extraFiles, 'excludes': ['*.pyc'], 'zip_include_packages': ['*'], 'zip_exclude_packages': [], 'include_msvcr': True, 'build_exe': '../../TreeLine-3.0'}}, executables = [Executable('treeline.py', base=base, icon='../win/treeline.ico')]) TreeLine/source/globalref.py0000644000175000017500000000355313363127527015071 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # globalref.py, provides a module for access to a few global variables # # TreeLine, an information storage program # Copyright (C) 2017, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** mainControl = None genOptions = None miscOptions = None histOptions = None toolbarOptions = None keyboardOptions = None toolIcons = None treeIcons = None localTextEncoding = '' lang = '' fileFilters = {'trlnopen': '{} (*.trln *.trln.gz *.trl)'. format(_('All TreeLine Files')), 'trlnv3': '{} (*.trln *.trln.gz)'.format(_('TreeLine Files')), 'trlnsave': '{} (*.trln)'.format(_('TreeLine Files')), 'trlngz': '{} (*.trln *.trln.gz)'. format(_('TreeLine Files - Compressed')), 'trlnenc': '{} (*.trln)'. format(_('TreeLine Files - Encrypted')), 'trl': '{} (*.trl *.xml)'.format(_('Old TreeLine Files')), 'all': '{} (*)'.format(_('All Files')), 'html': '{} (*.html *.htm)'.format(_('HTML Files')), 'txt': '{} (*.txt)'.format(_('Text Files')), 'xml': '{} (*.xml)'.format(_('XML Files')), 'csv': '{} (*.csv)'.format(_('CSV (Comma Delimited) Files')), 'odt': '{} (*.odt)'.format(_('ODF Text Files')), 'hjt': '{} (*.hjt)'.format(_('Treepad Files')), 'pdf': '{} (*.pdf)'.format(_('PDF Files'))} TreeLine/source/breadcrumbview.py0000644000175000017500000001563613363127527016142 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # breadcrumbview.py, provides a class for the breadcrumb view # # TreeLine, an information storage program # Copyright (C) 2017, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import operator from PyQt5.QtCore import QSize, Qt from PyQt5.QtGui import QPainter, QPalette from PyQt5.QtWidgets import (QAbstractItemView, QApplication, QStyledItemDelegate, QTableWidget, QTableWidgetItem) import globalref class CrumbItem(QTableWidgetItem): """Class to store breadcrumb item spot refs and positions. """ def __init__(self, spotRef): """Initialize the breadcrumb item. Arguments: spotRef -- ref to the associated spot item """ super().__init__(spotRef.nodeRef.title(spotRef)) self.spot = spotRef self.selectedSpot = False self.setTextAlignment(Qt.AlignCenter) self.setForeground(QApplication.palette().brush(QPalette.Link)) class BorderDelegate(QStyledItemDelegate): """Class override to show borders between rows. """ def __init__(self, parent=None): """Initialize the delegate class. Arguments: parent -- the parent view """ super().__init__(parent) def paint(self, painter, styleOption, modelIndex): """Paint the cells with borders between rows. """ super().paint(painter, styleOption, modelIndex) cell = self.parent().item(modelIndex.row(), modelIndex.column()) if modelIndex.row() > 0 and cell: upperCell = None row = modelIndex.row() while not upperCell and row > 0: row -= 1 upperCell = self.parent().item(row, modelIndex.column()) if cell.text() and upperCell and upperCell.text(): painter.drawLine(styleOption.rect.topLeft(), styleOption.rect.topRight()) class BreadcrumbView(QTableWidget): """Class override for the breadcrumb view. Sets view defaults and updates the content. """ def __init__(self, treeView, parent=None): """Initialize the breadcrumb view. Arguments: treeView - the tree view, needed for the current selection model parent -- the parent main window """ super().__init__(parent) self.treeView = treeView self.borderItems = [] self.setFocusPolicy(Qt.NoFocus) self.horizontalHeader().hide() self.verticalHeader().hide() self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.setSelectionMode(QAbstractItemView.NoSelection) self.setEditTriggers(QAbstractItemView.NoEditTriggers) self.setItemDelegate(BorderDelegate(self)) self.setShowGrid(False) self.setMouseTracking(True) self.itemClicked.connect(self.changeSelection) def updateContents(self): """Reload the view's content if the view is shown. Avoids update if view is not visible or has zero height or width. """ if not self.isVisible() or self.height() == 0 or self.width() == 0: return self.clear() self.clearSpans() selModel = self.treeView.selectionModel() selSpots = selModel.selectedSpots() if len(selSpots) != 1: return selSpot = selSpots[0] spotList = sorted(list(selSpot.nodeRef.spotRefs), key=operator.methodcaller('sortKey')) chainList = [[CrumbItem(chainSpot) for chainSpot in spot.spotChain()] for spot in spotList] self.setRowCount(len(chainList)) for row in range(len(chainList)): columns = len(chainList[row]) * 2 - 1 if columns > self.columnCount(): self.setColumnCount(columns) for col in range(len(chainList[row])): item = chainList[row][col] if (row == 0 or col >= len(chainList[row - 1]) or item.spot is not chainList[row - 1][col].spot): rowSpan = 1 while (row + rowSpan < len(chainList) and col < len(chainList[row + rowSpan]) and item.spot is chainList[row + rowSpan][col].spot): rowSpan += 1 if col < len(chainList[row]) - 1: arrowItem = QTableWidgetItem('\u25ba') arrowItem.setTextAlignment(Qt.AlignCenter) self.setItem(row, col * 2 + 1, arrowItem) if rowSpan > 1: self.setSpan(row, col * 2 + 1, rowSpan, 1) self.setItem(row, col * 2, item) if rowSpan > 1: self.setSpan(row, col * 2, rowSpan, 1) if item.spot is selSpot: item.selectedSpot = True item.setForeground(QApplication.palette(). brush(QPalette.WindowText)) self.resizeColumnsToContents() def changeSelection(self, item): """Change the current selection to given item bassed on a mouse click. Arguments: item -- the breadcrumb item that was clicked """ selModel = self.treeView.selectionModel() if hasattr(item, 'spot') and not item.selectedSpot: selModel.selectSpots([item.spot]) self.setCursor(Qt.ArrowCursor) def minimumSizeHint(self): """Set a short minimum size fint to allow the display of one row. """ return QSize(super().minimumSizeHint().width(), self.fontInfo().pixelSize() * 3) def mouseMoveEvent(self, event): """Change the mouse pointer if over a clickable item. Arguments: event -- the mouse move event """ item = self.itemAt(event.localPos().toPoint()) if item and hasattr(item, 'spot') and not item.selectedSpot: self.setCursor(Qt.PointingHandCursor) else: self.setCursor(Qt.ArrowCursor) super().mouseMoveEvent(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.updateContents() return super().resizeEvent(event) TreeLine/source/printdata.py0000644000175000017500000006451313745102623015117 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # printdata.py, provides a class for printing # # TreeLine, an information storage program # Copyright (C) 2019, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import os.path import enum from PyQt5.QtCore import QMarginsF, QSizeF, Qt from PyQt5.QtGui import (QAbstractTextDocumentLayout, QFontMetrics, QPageLayout, QPageSize, QPainter, QPalette, QTextDocument) from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QMessageBox from PyQt5.QtPrintSupport import QPrintDialog, QPrinter import treeoutput import printdialogs import globalref PrintScope = enum.IntEnum('PrintScope', 'entireTree selectBranch selectNode') _defaultMargin = 0.5 _defaultHeaderPos = 0.2 _defaultColumnSpace = 0.5 class PrintData: """Class to handle printing of tree output data. Stores print data and main printing functions. """ def __init__(self, localControl): """Initialize the print data. Arguments: localControl -- a reference to the parent local control """ self.localControl = localControl self.outputGroup = None self.printWhat = PrintScope.entireTree self.includeRoot = True self.openOnly = False self.printer = QPrinter(QPrinter.HighResolution) self.pageLayout = self.printer.pageLayout() self.setDefaults() self.adjustSpacing() def setDefaults(self): """Set all paparmeters saved in TreeLine files to default values. """ self.drawLines = True self.widowControl = True self.indentFactor = 2.0 self.pageLayout.setUnits(QPageLayout.Inch) self.pageLayout.setPageSize(QPageSize(QPageSize.Letter)) self.pageLayout.setOrientation(QPageLayout.Portrait) self.pageLayout.setMargins(QMarginsF(*(_defaultMargin,) * 4)) self.headerMargin = _defaultHeaderPos self.footerMargin = _defaultHeaderPos self.numColumns = 1 self.columnSpacing = _defaultColumnSpace self.headerText = '' self.footerText = '' self.useDefaultFont = True self.setDefaultFont() def setDefaultFont(self): """Set the default font initially and based on an output font change. """ self.defaultFont = QTextDocument().defaultFont() fontName = globalref.miscOptions['OutputFont'] if fontName: self.defaultFont.fromString(fontName) if self.useDefaultFont: self.mainFont = self.defaultFont def adjustSpacing(self): """Adjust line spacing & indent size based on font & indent factor. """ self.lineSpacing = QFontMetrics(self.mainFont, self.printer).lineSpacing() self.indentSize = self.indentFactor * self.lineSpacing def fileData(self): """Return a dictionary of non-default settings for storage. """ data = {} if not self.drawLines: data['printlines'] = False if not self.widowControl: data['printwidowcontrol'] = False if self.indentFactor != 2.0: data['printindentfactor'] = self.indentFactor pageSizeId = self.pageLayout.pageSize().id() if pageSizeId == QPageSize.Custom: paperWidth, paperHeight = self.roundedPaperSize() data['printpaperwidth'] = paperWidth data['printpaperheight'] = paperHeight elif pageSizeId != QPageSize.Letter: data['printpapersize'] = self.paperSizeName(pageSizeId) if self.pageLayout.orientation() != QPageLayout.Portrait: data['printportrait'] = False if self.roundedMargins() != (_defaultMargin,) * 4: data['printmargins'] = list(self.roundedMargins()) if self.headerMargin != _defaultHeaderPos: data['printheadermargin'] = self.headerMargin if self.footerMargin != _defaultHeaderPos: data['printfootermargin'] = self.footerMargin if self.numColumns > 1: data['printnumcolumns'] = self.numColumns if self.columnSpacing != _defaultColumnSpace: data['printcolumnspace'] = self.columnSpacing if self.headerText: data['printheadertext'] = self.headerText if self.footerText: data['printfootertext'] = self.footerText if not self.useDefaultFont: data['printfont'] = self.mainFont.toString() return data def readData(self, data): """Restore saved settings from a dictionary. Arguments: data -- a dictionary of stored non-default settings """ self.setDefaults() # necessary for undo/redo self.drawLines = data.get('printlines', True) self.widowControl = data.get('printwidowcontrol', True) self.indentFactor = data.get('printindentfactor', 2.0) if 'printpapersize' in data: self.pageLayout.setPageSize(QPageSize(getattr(QPageSize, data['printpapersize']))) self.pageLayout.setMargins(QMarginsF(*(_defaultMargin,) * 4)) if 'printpaperwidth' in data and 'printpaperheight' in data: width = data['printpaperwidth'] height = data['printpaperheight'] self.pageLayout.setPageSize(QPageSize(QSizeF(width, height), QPageSize.Inch)) self.pageLayout.setMargins(QMarginsF(*(_defaultMargin,) * 4)) if not data.get('printportrait', True): self.pageLayout.setOrientation(QPageLayout.Landscape) if 'printmargins' in data: margins = data['printmargins'] self.pageLayout.setMargins(QMarginsF(*margins)) self.headerMargin = data.get('printheadermargin', _defaultHeaderPos) self.footerMargin = data.get('printfootermargin', _defaultHeaderPos) self.numColumns = data.get('printnumcolumns', 1) self.columnSpacing = data.get('printcolumnspace', _defaultColumnSpace) self.headerText = data.get('printheadertext', '') self.footerText = data.get('printfootertext', '') if 'printfont' in data: self.useDefaultFont = False self.mainFont.fromString(data['printfont']) self.adjustSpacing() def roundedMargins(self): """Return a tuple of rounded page margins in inches. Rounds to nearest .01" to avoid Qt unit conversion artifacts. """ margins = self.pageLayout.margins(QPageLayout.Inch) return tuple(round(margin, 2) for margin in (margins.left(), margins.top(), margins.right(), margins.bottom())) def roundedPaperSize(self): """Return a tuple of rounded paper width and height. Rounds to nearest .01" to avoid Qt unit conversion artifacts. """ size = self.pageLayout.fullRect(QPageLayout.Inch) return (round(size.width(), 2), round(size.height(), 2)) def paperSizeName(self, sizeId=None): """Return a QPageSize attribute name matching the paper size ID. Arguments: sizeId -- the Qt size ID, if None, use current size """ if sizeId == None: sizeId = self.pageLayout.pageSize().id() matches = [] for name, num in vars(QPageSize).items(): if num == sizeId: matches.append(name) if not matches: return 'Custom' if len(matches) > 1: text = QPageSize(sizeId).name().split(None, 1)[0] for name in matches: if name == text: return name return matches[0] def setupData(self): """Load data to be printed and set page info. """ if self.printWhat == PrintScope.entireTree: selSpots = self.localControl.structure.rootSpots() else: selSpots = (self.localControl.currentSelectionModel(). selectedSpots()) if not selSpots: selSpots = self.localControl.structure.rootSpots() self.outputGroup = treeoutput.OutputGroup(selSpots, self.includeRoot, self.printWhat != PrintScope.selectNode, self.openOnly) self.paginate() def paginate(self): """Define the pages and locations of output items and set page range. """ pageNum = 1 columnNum = 0 pagePos = 0 itemSplit = False self.checkPageLayout() heightAvail = (self.pageLayout.paintRect().height() * self.printer.logicalDpiY()) columnSpacing = int(self.columnSpacing * self.printer.logicalDpiX()) widthAvail = ((self.pageLayout.paintRect().width() * self.printer.logicalDpiX() - columnSpacing * (self.numColumns - 1)) // self.numColumns) newGroup = treeoutput.OutputGroup([]) while self.outputGroup: item = self.outputGroup.pop(0) widthRemain = widthAvail - item.level * self.indentSize if pagePos != 0 and (newGroup[-1].addSpace or item.addSpace): pagePos += self.lineSpacing if item.siblingPrefix: siblings = treeoutput.OutputGroup([]) siblings.append(item) while True: item = siblings.combineLines() item.setDocHeight(self.printer, widthRemain, self.mainFont, True) if pagePos + item.height > heightAvail: self.outputGroup.insert(0, siblings.pop()) item = (siblings.combineLines() if siblings else None) break if (self.outputGroup and item.level == self.outputGroup[0].level and item.equalPrefix(self.outputGroup[0])): siblings.append(self.outputGroup.pop(0)) else: break if item: item.setDocHeight(self.printer, widthRemain, self.mainFont, True) if item.height > heightAvail and not itemSplit: item, newItem = item.splitDocHeight(heightAvail - pagePos, heightAvail, self.printer, widthRemain, self.mainFont) if newItem: self.outputGroup.insert(0, newItem) itemSplit = True if item and (pagePos + item.height <= heightAvail or pagePos == 0): item.pageNum = pageNum item.columnNum = columnNum item.pagePos = pagePos newGroup.append(item) pagePos += item.height else: if columnNum + 1 < self.numColumns: columnNum += 1 else: pageNum += 1 columnNum = 0 pagePos = 0 itemSplit = False if item: self.outputGroup.insert(0, item) if self.widowControl and not item.siblingPrefix: moveItems = [] moveHeight = 0 level = item.level while (newGroup and not newGroup[-1].siblingPrefix and newGroup[-1].level == level - 1 and ((newGroup[-1].pageNum == pageNum - 1 and newGroup[-1].columnNum == columnNum) or (newGroup[-1].pageNum == pageNum and newGroup[-1].columnNum == columnNum - 1))): moveItems.insert(0, newGroup.pop()) moveHeight += moveItems[0].height level -= 1 if (moveItems and newGroup and moveHeight < (heightAvail // 5)): self.outputGroup[0:0] = moveItems else: newGroup.extend(moveItems) self.outputGroup = newGroup self.outputGroup.loadFamilyRefs() self.printer.setFromTo(1, pageNum) def checkPageLayout(self): """Check and set the page layout on the current printer. Verify that the layout settings match the printer, adjust if required. """ if not self.printer.setPageLayout(self.pageLayout): tempPrinter = QPrinter() tempPageLayout = tempPrinter.pageLayout() tempPageLayout.setUnits(QPageLayout.Inch) pageSizeIssue = False defaultPageSize = tempPageLayout.pageSize() tempPageLayout.setPageSize(self.pageLayout.pageSize()) if not tempPrinter.setPageLayout(tempPageLayout): pageSizeIssue = True tempPageLayout.setPageSize(defaultPageSize) marginIssue = not (tempPageLayout.setMargins(self.pageLayout. margins()) and tempPrinter.setPageLayout(tempPageLayout)) if marginIssue: margin = 0.1 while True: if (tempPageLayout.setMargins(QMarginsF(*(margin,) * 4)) and tempPrinter.setPageLayout(tempPageLayout)): break margin += 0.1 newMargins = [] for oldMargin in self.roundedMargins(): newMargins.append(oldMargin if oldMargin >= margin else margin) tempPageLayout.setMargins(QMarginsF(*newMargins)) tempPageLayout.setOrientation(self.pageLayout.orientation()) self.printer.setPageLayout(tempPageLayout) if not pageSizeIssue and not marginIssue: return if pageSizeIssue and marginIssue: msg = _('Warning: Page size and margin settings unsupported ' 'on current printer.\nSave page adjustments?') elif pageSizeIssue: msg = _('Warning: Page size setting unsupported ' 'on current printer.\nSave adjustment?') else: msg = _('Warning: Margin settings unsupported ' 'on current printer.\nSave adjustments?') ans = QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if ans == QMessageBox.Yes: self.pageLayout = tempPageLayout def paintData(self, printer): """Paint data to be printed to the printer. """ pageNum = 1 try: maxPageNum = self.outputGroup[-1].pageNum except IndexError: # printing empty branch maxPageNum = 1 if self.printer.printRange() != QPrinter.AllPages: pageNum = self.printer.fromPage() maxPageNum = self.printer.toPage() painter = QPainter() if not painter.begin(self.printer): QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error initializing printer')) QApplication.setOverrideCursor(Qt.WaitCursor) while True: self.paintPage(pageNum, painter) if pageNum == maxPageNum: QApplication.restoreOverrideCursor() return pageNum += 1 self.printer.newPage() def paintPage(self, pageNum, painter): """Paint data for the given page to the printer. Arguments: pageNum -- the page number to be printed painter -- the painter for this print job """ paintContext = QAbstractTextDocumentLayout.PaintContext() # set context text color to black to wrok with dark app themes paintContext.palette = QPalette() paintContext.palette.setColor(QPalette.Text, Qt.black) try: totalNumPages = self.outputGroup[-1].pageNum except IndexError: # printing empty branch totalNumPages = 1 headerDoc = self.headerFooterDoc(True, pageNum, totalNumPages) if headerDoc: layout = headerDoc.documentLayout() layout.setPaintDevice(self.printer) headerDoc.setTextWidth(self.pageLayout.paintRect().width() * self.printer.logicalDpiX()) painter.save() topMargin = self.pageLayout.margins(QPageLayout.Inch).top() headerDelta = ((self.headerMargin - topMargin) * self.printer.logicalDpiX()) painter.translate(0, int(headerDelta)) layout.draw(painter, paintContext) painter.restore() painter.save() columnSpacing = int(self.columnSpacing * self.printer.logicalDpiX()) columnDelta = ((self.pageLayout.paintRect().width() * self.printer.logicalDpiX() - columnSpacing * (self.numColumns - 1)) / self.numColumns) + columnSpacing for columnNum in range(self.numColumns): if columnNum > 0: painter.translate(columnDelta, 0) self.paintColumn(pageNum, columnNum, painter, paintContext) painter.restore() footerDoc = self.headerFooterDoc(False, pageNum, totalNumPages) if footerDoc: layout = footerDoc.documentLayout() layout.setPaintDevice(self.printer) footerDoc.setTextWidth(self.pageLayout.paintRect().width() * self.printer.logicalDpiX()) painter.save() bottomMargin = self.pageLayout.margins(QPageLayout.Inch).bottom() footerDelta = ((bottomMargin - self.footerMargin) * self.printer.logicalDpiX()) painter.translate(0, self.pageLayout.paintRect().height() * self.printer.logicalDpiX() + int(footerDelta) - self.lineSpacing) layout.draw(painter, paintContext) painter.restore() def paintColumn(self, pageNum, columnNum, painter, paintContext): """Paint data for the given column to the printer. Arguments: pageNum -- the page number to be printed columnNum -- the column number to be printed painter -- the painter for this print job """ columnItems = [item for item in self.outputGroup if item.pageNum == pageNum and item.columnNum == columnNum] for item in columnItems: layout = item.doc.documentLayout() painter.save() painter.translate(item.level * self.indentSize, item.pagePos) layout.draw(painter, paintContext) painter.restore() if self.drawLines: self.addPrintLines(pageNum, columnNum, columnItems, painter) def addPrintLines(self, pageNum, columnNum, columnItems, painter): """Paint lines between parent and child items on the page. Arguments: pageNum -- the page number to be printed columnNum -- the column number to be printed columnItems -- a list of items in this column painter -- the painter for this print job """ parentsDrawn = set() horizOffset = self.indentSize // 2 vertOffset = self.lineSpacing // 2 heightAvail = (self.pageLayout.paintRect().height() * self.printer.logicalDpiY()) for item in columnItems: if item.level > 0: indent = item.level * self.indentSize vertPos = item.pagePos + vertOffset painter.drawLine(int(indent - horizOffset), int(vertPos), int(indent - self.lineSpacing // 4), int(vertPos)) parent = item.parentItem while parent: if parent in parentsDrawn: break lineStart = 0 lineEnd = heightAvail if (parent.pageNum == pageNum and parent.columnNum == columnNum): lineStart = parent.pagePos + parent.height if (parent.lastChildItem.pageNum == pageNum and parent.lastChildItem.columnNum == columnNum): lineEnd = parent.lastChildItem.pagePos + vertOffset if (parent.lastChildItem.pageNum > pageNum or (parent.lastChildItem.pageNum == pageNum and parent.lastChildItem.columnNum >= columnNum)): horizPos = ((parent.level + 1) * self.indentSize - horizOffset) painter.drawLine(int(horizPos), int(lineStart), int(horizPos), int(lineEnd)) parentsDrawn.add(parent) parent = parent.parentItem def formatHeaderFooter(self, header=True, pageNum=1, numPages=1): """Return an HTML table formatted header or footer. Return an empty string if no header/footer is defined. Arguments: header -- return header if True, footer if false """ if header: textParts = printdialogs.splitHeaderFooter(self.headerText) else: textParts = printdialogs.splitHeaderFooter(self.footerText) if not textParts: return '' fileInfoFormat = self.localControl.structure.treeFormats.fileInfoFormat fileInfoNode = self.localControl.structure.fileInfoNode fileInfoFormat.updateFileInfo(self.localControl.filePathObj, fileInfoNode) fileInfoNode.data[fileInfoFormat.pageNumFieldName] = repr(pageNum) fileInfoNode.data[fileInfoFormat.numPagesFieldName] = repr(numPages) fileInfoFormat.changeOutputLines(textParts, keepBlanks=True) textParts = fileInfoFormat.formatOutput(fileInfoNode, keepBlanks=True) alignments = ('left', 'center', 'right') result = [''] for text, align in zip(textParts, alignments): if text: result.append(''.format(align, text)) if len(result) > 1: result.append('
    {1}
    ') return '\n'.join(result) return '' def headerFooterDoc(self, header=True, pageNum=1, numPages=1): """Return a text document for the header or footer. Return None if no header/footer is defined. Arguments: header -- return header if True, footer if false """ text = self.formatHeaderFooter(header, pageNum, numPages) if text: doc = QTextDocument() doc.setHtml(text) doc.setDefaultFont(self.mainFont) frameFormat = doc.rootFrame().frameFormat() frameFormat.setBorder(0) frameFormat.setMargin(0) frameFormat.setPadding(0) doc.rootFrame().setFrameFormat(frameFormat) return doc return None def printSetup(self): """Show a dialog to set margins, page size and other printing options. """ setupDialog = printdialogs.PrintSetupDialog(self, True, QApplication. activeWindow()) setupDialog.exec_() def printPreview(self): """Show a preview of printing results. """ self.setupData() previewDialog = printdialogs.PrintPreviewDialog(self,QApplication. activeWindow()) previewDialog.previewWidget.paintRequested.connect(self.paintData) if globalref.genOptions['SaveWindowGeom']: previewDialog.restoreDialogGeom() previewDialog.exec_() def filePrint(self): """Show dialog and print tree output based on current options. """ self.printer.setOutputFormat(QPrinter.NativeFormat) self.setupData() printDialog = QPrintDialog(self.printer, QApplication.activeWindow()) if printDialog.exec_() == QDialog.Accepted: self.paintData(self.printer) def filePrintPdf(self): """Export to a PDF file with current options. """ filters = ';;'.join((globalref.fileFilters['pdf'], globalref.fileFilters['all'])) defaultFilePath = str(globalref.mainControl.defaultPathObj()) defaultFilePath = os.path.splitext(defaultFilePath)[0] if os.path.basename(defaultFilePath): defaultFilePath = '{0}.{1}'.format(defaultFilePath, 'pdf') filePath, selectFilter = QFileDialog.getSaveFileName(QApplication. activeWindow(), _('TreeLine - Export PDF'), defaultFilePath, filters) if not filePath: return if not os.path.splitext(filePath)[1]: filePath = '{0}.{1}'.format(filePath, 'pdf') origFormat = self.printer.outputFormat() self.printer.setOutputFormat(QPrinter.PdfFormat) self.printer.setOutputFileName(filePath) self.adjustSpacing() self.setupData() self.paintData(self.printer) self.printer.setOutputFormat(origFormat) self.printer.setOutputFileName('') self.adjustSpacing() TreeLine/source/configdialog.py0000644000175000017500000031612213745103751015555 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # configdialog.py, provides classes for the type configuration dialog # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import copy import operator from PyQt5.QtCore import QPoint, QSize, Qt, pyqtSignal from PyQt5.QtGui import QTextCursor from PyQt5.QtWidgets import (QAbstractItemView, QApplication, QButtonGroup, QCheckBox, QComboBox, QDialog, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListView, QListWidget, QListWidgetItem, QMenu, QMessageBox, QPushButton, QScrollArea, QSizePolicy, QSpinBox, QTabWidget, QTextEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget) import nodeformat import fieldformat import icondict import conditional import matheval import globalref class ConfigDialog(QDialog): """Class override for the main config dialog Contains the tabbed pages that handle the actual settings. """ dialogShown = pyqtSignal(bool) treeStruct = None formatsRef = None currentTypeName = '' currentFieldName = '' def __init__(self, parent=None): """Initialize the config dialog. Arguments: parent -- the parent window """ super().__init__(parent) self.setAttribute(Qt.WA_QuitOnClose, False) self.setWindowFlags(Qt.Window) self.setWindowTitle(_('Configure Data Types')) self.prevPage = None self.localControl = None self.selectionModel = None topLayout = QVBoxLayout(self) self.setLayout(topLayout) self.tabs = QTabWidget() topLayout.addWidget(self.tabs) typeListPage = TypeListPage(self) self.tabs.addTab(typeListPage, _('T&ype List')) typeConfigPage = TypeConfigPage(self) self.tabs.addTab(typeConfigPage, _('Typ&e Config')) fieldListPage = FieldListPage(self) self.tabs.addTab(fieldListPage, _('Field &List')) fieldConfigPage = FieldConfigPage(self) self.tabs.addTab(fieldConfigPage, _('&Field Config')) outputPage = OutputPage(self) self.tabs.addTab(outputPage, _('O&utput')) self.tabs.currentChanged.connect(self.updatePage) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) self.advancedButton = QPushButton(_('&Show Advanced')) ctrlLayout.addWidget(self.advancedButton) self.advancedButton.setCheckable(True) self.advancedButton.clicked.connect(self.toggleAdavanced) ctrlLayout.addStretch() okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.applyAndClose) self.applyButton = QPushButton(_('&Apply')) ctrlLayout.addWidget(self.applyButton) self.applyButton.clicked.connect(self.applyChanges) self.resetButton = QPushButton(_('&Reset')) ctrlLayout.addWidget(self.resetButton) self.resetButton.clicked.connect(self.reset) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.resetAndClose) def setRefs(self, localControl, resetSelect=False, forceCopy=False): """Set refs to model and formats, then update dialog data. Sets current type to current node's type if resetSelect or if invalid. Sets current field to first field if resetSelect or if invalid. Arguments: localControl -- a reference to the local control resetSelect -- if True, forces reset of current selections forceCopy -- if True, force making a new copy of formats """ self.localControl = localControl ConfigDialog.treeStruct = localControl.structure ConfigDialog.formatsRef = (ConfigDialog.treeStruct. getConfigDialogFormats(forceCopy)) self.selectionModel = localControl.currentSelectionModel() self.updateSelections(resetSelect) self.setModified(modified=False) self.prevPage = None self.updatePage() def updateSelections(self, forceUpdate=False): """Sets current type & current field if invalid or forceUpdate is True. Arguments: forceUpdate -- if True, forces reset of current selections """ if forceUpdate or (ConfigDialog.currentTypeName not in ConfigDialog.formatsRef): try: ConfigDialog.currentTypeName = (self.selectionModel. currentNode().formatRef.name) except AttributeError: # no current node ConfigDialog.currentTypeName = (ConfigDialog.treeStruct. childList[0].formatRef.name) if forceUpdate or (ConfigDialog.currentFieldName not in ConfigDialog.formatsRef[ConfigDialog. currentTypeName].fieldNames()): ConfigDialog.currentFieldName = (ConfigDialog. formatsRef[ConfigDialog. currentTypeName]. fieldNames()[0]) 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 setModified(self, dummyArg=None, modified=True): """Set the format to a modified status and update the controls. Arguments: dummyArg -- placeholder for unused signal arguments modified -- set to modified if True """ ConfigDialog.formatsRef.configModified = modified self.applyButton.setEnabled(modified) self.resetButton.setEnabled(modified) def toggleAdavanced(self, show): """Toggle the display of advanced widgets in the sub-dialogs. Arguments: show -- show if true, hide if false """ if show: self.advancedButton.setText(_('&Hide Advanced')) else: self.advancedButton.setText(_('&Show Advanced')) page = self.tabs.currentWidget() page.toggleAdvanced(show) def reset(self): """Set the formats back to original settings. """ ConfigDialog.formatsRef = (ConfigDialog.treeStruct. getConfigDialogFormats(True)) self.updateSelections() self.setModified(modified=False) self.prevPage = None self.updatePage() def applyChanges(self): """Apply copied format changes to the main format. Return False if there is a circular math reference. """ self.tabs.currentWidget().readChanges() if ConfigDialog.formatsRef.configModified: try: ConfigDialog.treeStruct.applyConfigDialogFormats() except matheval.CircularMathError: QMessageBox.warning(self, 'TreeLine', _('Error - circular reference in math field equations')) return False self.setModified(modified=False) self.localControl.updateAll() return True def applyAndClose(self): """Apply copied format changes to the main format and close the dialog. """ if self.applyChanges(): self.close() def resetAndClose(self): """Set the formats back to original settings and close the dialog. """ self.reset() self.close() def closeEvent(self, event): """Signal that the dialog is closing. Arguments: event -- the close event """ self.dialogShown.emit(False) class ConfigPage(QWidget): """Abstract base class for config dialog tabbed pages. """ def __init__(self, parent=None): """Initialize the config dialog page. Arguments: parent -- the parent overall dialog """ super().__init__(parent) self.mainDialogRef = parent self.advancedWidgets = [] def updateContent(self): """Update page contents from current format settings. Base class does nothing. """ pass def readChanges(self): """Make changes to the format for each widget. Base class does nothing. """ pass def changeCurrentType(self, typeName): """Change the current format type based on a signal from lists. Arguments: typeName -- the name of the new current type """ self.readChanges() ConfigDialog.currentTypeName = typeName ConfigDialog.currentFieldName = (ConfigDialog.formatsRef[typeName]. fieldNames()[0]) if type(self) != TypeListPage: # "if" statement added to work around list view selection bug self.updateContent() def changeCurrentField(self, fieldName): """Change the current format field based on a signal from lists. Arguments: fieldName -- the name of the new current field """ self.readChanges() ConfigDialog.currentFieldName = fieldName self.updateContent() def toggleAdvanced(self, show=True): """Toggle the display state of advanced widgets. Arguments: show -- show if true, hide if false """ for widget in self.advancedWidgets: widget.setVisible(show) class TypeListPage(ConfigPage): """Config dialog page with an editable list of node types. """ def __init__(self, parent=None): """Initialize the config dialog page. Arguments: parent -- the parent overall dialog """ super().__init__(parent) topLayout = QVBoxLayout(self) box = QGroupBox(_('Add or Remove Data Types')) topLayout.addWidget(box) horizLayout = QHBoxLayout(box) self.listBox = QListWidget() self.listBox.setSelectionMode(QAbstractItemView.SingleSelection) horizLayout.addWidget(self.listBox) self.listBox.currentTextChanged.connect(self.changeCurrentType) buttonLayout = QVBoxLayout() horizLayout.addLayout(buttonLayout) newButton = QPushButton(_('&New Type...')) buttonLayout.addWidget(newButton) newButton.clicked.connect(self.newType) copyButton = QPushButton(_('Co&py Type...')) buttonLayout.addWidget(copyButton) copyButton.clicked.connect(self.copyType) renameButton = QPushButton(_('Rena&me Type...')) buttonLayout.addWidget(renameButton) renameButton.clicked.connect(self.renameType) deleteButton = QPushButton(_('&Delete Type')) buttonLayout.addWidget(deleteButton) deleteButton.clicked.connect(self.deleteType) def updateContent(self): """Update page contents from current format settings. """ names = ConfigDialog.formatsRef.typeNames() self.listBox.blockSignals(True) self.listBox.clear() self.listBox.addItems(names) self.listBox.setCurrentRow(names.index(ConfigDialog.currentTypeName)) self.listBox.blockSignals(False) def newType(self): """Create a new type based on button signal. """ dlg = NameEntryDialog(_('Add Type'), _('Enter new type name:'), '', '', ConfigDialog.formatsRef.typeNames(), self) if dlg.exec_() == QDialog.Accepted: newFormat = nodeformat.NodeFormat(dlg.text, ConfigDialog.formatsRef, None, True) ConfigDialog.formatsRef[dlg.text] = newFormat ConfigDialog.currentTypeName = dlg.text ConfigDialog.currentFieldName = newFormat.fieldNames()[0] self.updateContent() self.mainDialogRef.setModified() def copyType(self): """Copy selected type based on button signal. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] dlg = NameEntryDialog(_('Copy Type'), _('Enter new type name:'), ConfigDialog.currentTypeName, _('&Derive from original'), ConfigDialog.formatsRef.typeNames(), self) if currentFormat.genericType: dlg.extraCheckBox.setEnabled(False) if dlg.exec_() == QDialog.Accepted: newFormat = copy.deepcopy(currentFormat) newFormat.name = dlg.text # avoid using copied reference for parentFormats newFormat.parentFormats = currentFormat.parentFormats ConfigDialog.formatsRef[dlg.text] = newFormat ConfigDialog.currentTypeName = dlg.text if dlg.extraChecked: newFormat.genericType = currentFormat.name ConfigDialog.formatsRef.updateDerivedRefs() self.updateContent() self.mainDialogRef.setModified() def renameType(self): """Rename the selected type based on button signal. """ oldName = ConfigDialog.currentTypeName dlg = NameEntryDialog(_('Rename Type'), _('Rename from {} to:').format(oldName), oldName, '', ConfigDialog.formatsRef.typeNames(), self) if dlg.exec_() == QDialog.Accepted: currentType = ConfigDialog.formatsRef[oldName] currentType.name = dlg.text del ConfigDialog.formatsRef[oldName] ConfigDialog.formatsRef[dlg.text] = currentType # reverse the rename dict - find original name (multiple renames) reverseDict = {} for old, new in ConfigDialog.formatsRef.typeRenameDict.items(): reverseDict[new] = old origName = reverseDict.get(oldName, oldName) ConfigDialog.formatsRef.typeRenameDict[origName] = dlg.text if oldName in ConfigDialog.formatsRef.fieldRenameDict: ConfigDialog.formatsRef.fieldRenameDict[dlg.text] = \ ConfigDialog.formatsRef.fieldRenameDict[oldName] del ConfigDialog.formatsRef.fieldRenameDict[oldName] for nodeType in ConfigDialog.formatsRef.values(): if nodeType.childType == oldName: nodeType.childType = dlg.text if nodeType.genericType == oldName: nodeType.genericType = dlg.text if oldName in nodeType.childTypeLimit: nodeType.childTypeLimit.remove(oldName) nodeType.childTypeLimit.add(dlg.text) ConfigDialog.currentTypeName = dlg.text self.updateContent() self.mainDialogRef.setModified() def deleteType(self): """Delete the selected type based on button signal. """ # reverse the rename dict - find original name (before any rename) reverseDict = {} for old, new in ConfigDialog.formatsRef.typeRenameDict.items(): reverseDict[new] = old origName = reverseDict.get(ConfigDialog.currentTypeName, ConfigDialog.currentTypeName) if ConfigDialog.treeStruct.usesType(origName): QMessageBox.warning(self, 'TreeLine', _('Cannot delete data type being used by nodes')) return del ConfigDialog.formatsRef[ConfigDialog.currentTypeName] if origName != ConfigDialog.currentTypeName: del ConfigDialog.formatsRef.typeRenameDict[origName] for nodeType in ConfigDialog.formatsRef.values(): if nodeType.childType == ConfigDialog.currentTypeName: nodeType.childType = '' if nodeType.genericType == ConfigDialog.currentTypeName: nodeType.genericType = '' nodeType.conditional = None nodeType.childTypeLimit.discard(ConfigDialog.currentTypeName) ConfigDialog.formatsRef.updateDerivedRefs() ConfigDialog.currentTypeName = ConfigDialog.formatsRef.typeNames()[0] ConfigDialog.currentFieldName = ConfigDialog.formatsRef[ConfigDialog. currentTypeName].fieldNames()[0] self.updateContent() self.mainDialogRef.setModified() _noTypeSetName = _('[None]', 'no type set') class TypeConfigPage(ConfigPage): """Config dialog page to change parmaters of a node type. """ def __init__(self, parent=None): """Initialize the config dialog page. Arguments: parent -- the parent overall dialog """ super().__init__(parent) topLayout = QGridLayout(self) typeBox = QGroupBox(_('&Data Type')) topLayout.addWidget(typeBox, 0, 0) typeLayout = QVBoxLayout(typeBox) self.typeCombo = QComboBox() typeLayout.addWidget(self.typeCombo) self.typeCombo.currentIndexChanged[str].connect(self.changeCurrentType) childBox = QGroupBox(_('Default Child &Type')) topLayout.addWidget(childBox, 0, 1) childLayout = QVBoxLayout(childBox) self.childCombo = QComboBox() childLayout.addWidget(self.childCombo) self.childCombo.currentIndexChanged.connect(self.mainDialogRef. setModified) iconBox = QGroupBox(_('Icon')) topLayout.addWidget(iconBox, 1, 1) iconLayout = QHBoxLayout(iconBox) self.iconImage = QLabel() iconLayout.addWidget(self.iconImage) self.iconImage.setAlignment(Qt.AlignCenter) iconButton = QPushButton(_('Change &Icon')) iconLayout.addWidget(iconButton) iconButton.clicked.connect(self.changeIcon) optionsBox = QGroupBox(_('Output Options')) topLayout.addWidget(optionsBox, 1, 0, 2, 1) optionsLayout = QVBoxLayout(optionsBox) self.blanksButton = QCheckBox(_('Add &blank lines between ' 'nodes')) optionsLayout.addWidget(self.blanksButton) self.blanksButton.toggled.connect(self.mainDialogRef.setModified) self.htmlButton = QCheckBox(_('Allow &HTML rich text in format')) optionsLayout.addWidget(self.htmlButton) self.htmlButton.toggled.connect(self.mainDialogRef.setModified) self.bulletButton = QCheckBox(_('Add text bullet&s')) optionsLayout.addWidget(self.bulletButton) self.bulletButton.toggled.connect(self.changeUseBullets) self.tableButton = QCheckBox(_('Use a table for field &data')) optionsLayout.addWidget(self.tableButton) self.tableButton.toggled.connect(self.changeUseTable) # advanced widgets outputSepBox = QGroupBox(_('Combination && Child List Output ' '&Separator')) topLayout.addWidget(outputSepBox, 2, 1) self.advancedWidgets.append(outputSepBox) outputSepLayout = QVBoxLayout(outputSepBox) self.outputSepEdit = QLineEdit() outputSepLayout.addWidget(self.outputSepEdit) sizePolicy = self.outputSepEdit.sizePolicy() sizePolicy.setHorizontalPolicy(QSizePolicy.Preferred) self.outputSepEdit.setSizePolicy(sizePolicy) self.outputSepEdit.textEdited.connect(self.mainDialogRef.setModified) genericBox = QGroupBox(_('Derived from &Generic Type')) topLayout.addWidget(genericBox, 3, 0) self.advancedWidgets.append(genericBox) genericLayout = QVBoxLayout(genericBox) self.genericCombo = QComboBox() genericLayout.addWidget(self.genericCombo) self.genericCombo.currentIndexChanged.connect(self.setConditionAvail) self.genericCombo.currentIndexChanged.connect(self.mainDialogRef. setModified) conditionBox = QGroupBox(_('Automatic Types')) topLayout.addWidget(conditionBox, 3, 1) self.advancedWidgets.append(conditionBox) conditionLayout = QVBoxLayout(conditionBox) self.conditionButton = QPushButton() conditionLayout.addWidget(self.conditionButton) self.conditionButton.clicked.connect(self.showConditionDialog) typeLimitBox = QGroupBox(_('Child Type Limits')) topLayout.addWidget(typeLimitBox, 4, 0) self.advancedWidgets.append(typeLimitBox) typeLimitLayout = QVBoxLayout(typeLimitBox) self.typeLimitCombo = TypeLimitCombo() typeLimitLayout.addWidget(self.typeLimitCombo) self.typeLimitCombo.limitChanged.connect(self.mainDialogRef. setModified) topLayout.setRowStretch(5, 1) def updateContent(self): """Update page contents from current format settings. """ typeNames = ConfigDialog.formatsRef.typeNames() self.typeCombo.blockSignals(True) self.typeCombo.clear() self.typeCombo.addItems(typeNames) self.typeCombo.setCurrentIndex(typeNames.index(ConfigDialog. currentTypeName)) self.typeCombo.blockSignals(False) currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] self.childCombo.blockSignals(True) self.childCombo.clear() self.childCombo.addItem(_noTypeSetName) self.childCombo.addItems(typeNames) try: childItem = typeNames.index(currentFormat.childType) + 1 except ValueError: childItem = 0 self.childCombo.setCurrentIndex(childItem) self.childCombo.blockSignals(False) icon = globalref.treeIcons.getIcon(currentFormat.iconName, True) if icon: self.iconImage.setPixmap(icon.pixmap(16, 16)) else: self.iconImage.setText(_('None')) self.blanksButton.blockSignals(True) self.blanksButton.setChecked(currentFormat.spaceBetween) self.blanksButton.blockSignals(False) self.htmlButton.blockSignals(True) self.htmlButton.setChecked(currentFormat.formatHtml) self.htmlButton.blockSignals(False) self.bulletButton.blockSignals(True) self.bulletButton.setChecked(currentFormat.useBullets) self.bulletButton.blockSignals(False) self.tableButton.blockSignals(True) self.tableButton.setChecked(currentFormat.useTables) self.tableButton.blockSignals(False) self.htmlButton.setEnabled(not currentFormat.useBullets and not currentFormat.useTables) self.outputSepEdit.setText(currentFormat.outputSeparator) self.genericCombo.blockSignals(True) self.genericCombo.clear() self.genericCombo.addItem(_noTypeSetName) genTypeNames = [name for name in typeNames if name != ConfigDialog.currentTypeName and not ConfigDialog.formatsRef[name].genericType] self.genericCombo.addItems(genTypeNames) try: generic = genTypeNames.index(currentFormat.genericType) + 1 except ValueError: generic = 0 self.genericCombo.setCurrentIndex(generic) self.genericCombo.blockSignals(False) self.setConditionAvail() self.typeLimitCombo.updateLists(typeNames, currentFormat.childTypeLimit) def changeIcon(self): """Show the change icon dialog based on a button press. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] dlg = IconSelectDialog(currentFormat, self) if (dlg.exec_() == QDialog.Accepted and dlg.currentIconName != currentFormat.iconName): currentFormat.iconName = dlg.currentIconName self.mainDialogRef.setModified() self.updateContent() def changeUseBullets(self, checked=True): """Change setting to use bullets for output. Does not allow bullets and table to both be checked, and automatically checks use HTML. Arguments: checked -- True if bullets are selected """ if checked: self.tableButton.setChecked(False) self.htmlButton.setChecked(True) self.htmlButton.setEnabled(not checked) self.mainDialogRef.setModified() def changeUseTable(self, checked=True): """Change setting to use tables for output. Does not allow bullets and table to both be checked, and automatically checks use HTML. Arguments: checked -- True if tables are selected """ if checked: self.bulletButton.setChecked(False) self.htmlButton.setChecked(True) self.htmlButton.setEnabled(not checked) self.mainDialogRef.setModified() def setConditionAvail(self): """Enable conditional button if generic or dervived type. Set button text based on presence of conditions. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] if self.genericCombo.currentIndex() > 0 or currentFormat.derivedTypes: self.conditionButton.setEnabled(True) if currentFormat.conditional: self.conditionButton.setText(_('Modify Co&nditional Types')) return else: self.conditionButton.setEnabled(False) self.conditionButton.setText(_('Create Co&nditional Types')) def showConditionDialog(self): """Show the dialog to create or modify conditional types. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] dialog = conditional.ConditionDialog(conditional.FindDialogType. typeDialog, _('Set Types Conditionally'), currentFormat) if currentFormat.conditional: dialog.setCondition(currentFormat.conditional) if dialog.exec_() == QDialog.Accepted: currentFormat.conditional = dialog.conditional() if not currentFormat.conditional: currentFormat.conditional = None ConfigDialog.formatsRef.updateDerivedRefs() self.mainDialogRef.setModified() self.updateContent() def readChanges(self): """Make changes to the format for each widget. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] currentFormat.childType = self.childCombo.currentText() if currentFormat.childType == _noTypeSetName: currentFormat.childType = '' currentFormat.outputSeparator = self.outputSepEdit.text() prevGenericType = currentFormat.genericType currentFormat.genericType = self.genericCombo.currentText() if currentFormat.genericType == _noTypeSetName: currentFormat.genericType = '' if currentFormat.genericType != prevGenericType: ConfigDialog.formatsRef.updateDerivedRefs() currentFormat.updateFromGeneric(formatsRef=ConfigDialog.formatsRef) if ConfigDialog.currentFieldName not in currentFormat.fieldNames(): ConfigDialog.currentFieldName = currentFormat.fieldNames()[0] currentFormat.spaceBetween = self.blanksButton.isChecked() currentFormat.formatHtml = self.htmlButton.isChecked() currentFormat.childTypeLimit = self.typeLimitCombo.selectSet useBullets = self.bulletButton.isChecked() useTables = self.tableButton.isChecked() if (useBullets != currentFormat.useBullets or useTables != currentFormat.useTables): currentFormat.useBullets = useBullets currentFormat.useTables = useTables if useBullets: currentFormat.addBullets() elif useTables: currentFormat.addTables() else: currentFormat.clearBulletsAndTables() class FieldListPage(ConfigPage): """Config dialog page with an editable list of fields. """ def __init__(self, parent=None): """Initialize the config dialog page. Arguments: parent -- the parent overall dialog """ super().__init__(parent) topLayout = QVBoxLayout(self) typeBox = QGroupBox(_('&Data Type')) topLayout.addWidget(typeBox) typeLayout = QVBoxLayout(typeBox) self.typeCombo = QComboBox() typeLayout.addWidget(self.typeCombo) self.typeCombo.currentIndexChanged[str].connect(self.changeCurrentType) fieldBox = QGroupBox(_('Modify &Field List')) topLayout.addWidget(fieldBox) horizLayout = QHBoxLayout(fieldBox) self.fieldListBox = QTreeWidget() horizLayout.addWidget(self.fieldListBox) self.fieldListBox.setRootIsDecorated(False) self.fieldListBox.setColumnCount(3) self.fieldListBox.setHeaderLabels([_('Name'), _('Type'), _('Sort Key')]) self.fieldListBox.currentItemChanged.connect(self.changeField) buttonLayout = QVBoxLayout() horizLayout.addLayout(buttonLayout) self.upButton = QPushButton(_('Move U&p')) buttonLayout.addWidget(self.upButton) self.upButton.clicked.connect(self.moveUp) self.downButton = QPushButton(_('Move Do&wn')) buttonLayout.addWidget(self.downButton) self.downButton.clicked.connect(self.moveDown) self.newButton = QPushButton(_('&New Field...')) buttonLayout.addWidget(self.newButton) self.newButton.clicked.connect(self.newField) self.renameButton = QPushButton(_('Rena&me Field...')) buttonLayout.addWidget(self.renameButton) self.renameButton.clicked.connect(self.renameField) self.deleteButton = QPushButton(_('Dele&te Field')) buttonLayout.addWidget(self.deleteButton) self.deleteButton.clicked.connect(self.deleteField) sortKeyButton = QPushButton(_('Sort &Keys...')) buttonLayout.addWidget(sortKeyButton) sortKeyButton.clicked.connect(self.defineSortKeys) def updateContent(self): """Update page contents from current format settings. """ typeNames = ConfigDialog.formatsRef.typeNames() self.typeCombo.blockSignals(True) self.typeCombo.clear() self.typeCombo.addItems(typeNames) self.typeCombo.setCurrentIndex(typeNames.index(ConfigDialog. currentTypeName)) self.typeCombo.blockSignals(False) currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] sortFields = [field for field in currentFormat.fields() if field.sortKeyNum > 0] sortFields.sort(key = operator.attrgetter('sortKeyNum')) if not sortFields: sortFields = [list(currentFormat.fields())[0]] self.fieldListBox.blockSignals(True) self.fieldListBox.clear() for field in currentFormat.fields(): try: sortKey = repr(sortFields.index(field) + 1) sortDir = _('fwd') if field.sortKeyForward else _('rev') sortKey = '{0} ({1})'.format(sortKey, sortDir) except ValueError: sortKey = '' typeName = fieldformat.translatedTypeName(field.typeName) QTreeWidgetItem(self.fieldListBox, [field.name, typeName, sortKey]) selectNum = currentFormat.fieldNames().index(ConfigDialog. currentFieldName) selectItem = self.fieldListBox.topLevelItem(selectNum) self.fieldListBox.setCurrentItem(selectItem) selectItem.setSelected(True) width = self.fieldListBox.viewport().width() self.fieldListBox.setColumnWidth(0, int(width // 2.5)) self.fieldListBox.setColumnWidth(1, int(width // 2.5)) self.fieldListBox.setColumnWidth(2, width // 5) self.fieldListBox.blockSignals(False) num = currentFormat.fieldNames().index(ConfigDialog.currentFieldName) self.upButton.setEnabled(num > 0 and not currentFormat.genericType) self.downButton.setEnabled(num < len(currentFormat.fieldDict) - 1 and not currentFormat.genericType) self.newButton.setEnabled(not currentFormat.genericType) self.renameButton.setEnabled(not currentFormat.genericType) self.deleteButton.setEnabled(len(currentFormat.fieldDict) > 1 and not currentFormat.genericType) def changeField(self, currentItem, prevItem): """Change the current format field based on a tree widget signal. Arguments: currentItem -- the new current tree widget item prevItem -- the old current tree widget item """ self.changeCurrentField(currentItem.text(0)) def moveUp(self): """Move field upward in the list based on button signal. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] fieldList = currentFormat.fieldNames() num = fieldList.index(ConfigDialog.currentFieldName) if num > 0: fieldList[num-1], fieldList[num] = fieldList[num], fieldList[num-1] currentFormat.reorderFields(fieldList) currentFormat.updateDerivedTypes() self.updateContent() self.mainDialogRef.setModified() def moveDown(self): """Move field downward in the list based on button signal. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] fieldList = currentFormat.fieldNames() num = fieldList.index(ConfigDialog.currentFieldName) if num < len(fieldList) - 1: fieldList[num], fieldList[num+1] = fieldList[num+1], fieldList[num] currentFormat.reorderFields(fieldList) currentFormat.updateDerivedTypes() self.updateContent() self.mainDialogRef.setModified() def newField(self): """Create and add a new field based on button signal. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] dlg = NameEntryDialog(_('Add Field'), _('Enter new field name:'), '', '', currentFormat.fieldNames(), self) if dlg.exec_() == QDialog.Accepted: currentFormat.addField(dlg.text) ConfigDialog.currentFieldName = dlg.text currentFormat.updateDerivedTypes() self.updateContent() self.mainDialogRef.setModified() def renameField(self): """Prompt for new name and rename field based on button signal. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] fieldList = currentFormat.fieldNames() oldName = ConfigDialog.currentFieldName dlg = NameEntryDialog(_('Rename Field'), _('Rename from {} to:').format(oldName), oldName, '', fieldList, self) if dlg.exec_() == QDialog.Accepted: num = fieldList.index(oldName) fieldList[num] = dlg.text for nodeFormat in [currentFormat] + currentFormat.derivedTypes: field = nodeFormat.fieldDict[oldName] field.name = dlg.text nodeFormat.fieldDict[field.name] = field nodeFormat.reorderFields(fieldList) if nodeFormat.conditional: nodeFormat.conditional.renameFields(oldName, field.name) # savedConditions = {} # for name, text in nodeFormat.savedConditionText.items(): # condition = conditional.Conditional(text, nodeFormat.name) # condition.renameFields(oldName, field.name) # savedConditions[name] = condition.conditionStr() # nodeFormat.savedConditionText = savedConditions renameDict = (ConfigDialog.formatsRef.fieldRenameDict. setdefault(nodeFormat.name, {})) # reverse rename dict - find original name (multiple renames) reverseDict = {} for old, new in renameDict.items(): reverseDict[new] = old origName = reverseDict.get(oldName, oldName) renameDict[origName] = dlg.text ConfigDialog.currentFieldName = dlg.text self.updateContent() self.mainDialogRef.setModified() def deleteField(self): """Delete field based on button signal. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] num = currentFormat.fieldNames().index(ConfigDialog.currentFieldName) for nodeFormat in [currentFormat] + currentFormat.derivedTypes: field = nodeFormat.fieldDict[ConfigDialog.currentFieldName] nodeFormat.removeField(field) del nodeFormat.fieldDict[ConfigDialog.currentFieldName] if num > 0: num -= 1 ConfigDialog.currentFieldName = currentFormat.fieldNames()[num] ConfigDialog.formatsRef.updateDerivedRefs() self.updateContent() self.mainDialogRef.setModified() def defineSortKeys(self): """Show a dialog to change sort key fields and directions. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] dlg = SortKeyDialog(currentFormat.fieldDict, self) if dlg.exec_() == QDialog.Accepted: self.updateContent() self.mainDialogRef.setModified() _fileInfoFormatName = _('File Info Reference') class FieldConfigPage(ConfigPage): """Config dialog page to change parmaters of a field. """ def __init__(self, parent=None): """Initialize the config dialog page. Arguments: parent -- the parent overall dialog """ super().__init__(parent) self.currentFileInfoField = '' topLayout = QGridLayout(self) typeBox = QGroupBox(_('&Data Type')) topLayout.addWidget(typeBox, 0, 0) typeLayout = QVBoxLayout(typeBox) self.typeCombo = QComboBox() typeLayout.addWidget(self.typeCombo) self.typeCombo.currentIndexChanged[str].connect(self.changeCurrentType) fieldBox = QGroupBox(_('F&ield')) topLayout.addWidget(fieldBox, 0, 1) fieldLayout = QVBoxLayout(fieldBox) self.fieldCombo = QComboBox() fieldLayout.addWidget(self.fieldCombo) self.fieldCombo.currentIndexChanged[str].connect(self. changeCurrentField) fieldTypeBox = QGroupBox(_('&Field Type')) topLayout.addWidget(fieldTypeBox, 1, 0) fieldTypeLayout = QVBoxLayout(fieldTypeBox) self.fieldTypeCombo = QComboBox() fieldTypeLayout.addWidget(self.fieldTypeCombo) self.fieldTypeCombo.addItems(fieldformat.translatedFieldTypes) self.fieldTypeCombo.currentIndexChanged.connect(self.changeFieldType) self.formatBox = QGroupBox(_('Outpu&t Format')) topLayout.addWidget(self.formatBox, 1, 1) formatLayout = QHBoxLayout(self.formatBox) self.formatEdit = QLineEdit() formatLayout.addWidget(self.formatEdit) self.formatEdit.textEdited.connect(self.mainDialogRef.setModified) self.helpButton = QPushButton(_('Format &Help')) formatLayout.addWidget(self.helpButton) self.helpButton.clicked.connect(self.formatHelp) extraBox = QGroupBox(_('Extra Text')) topLayout.addWidget(extraBox, 2, 0, 2, 1) extraLayout = QVBoxLayout(extraBox) extraLayout.setSpacing(0) prefixLabel = QLabel(_('&Prefix')) extraLayout.addWidget(prefixLabel) self.prefixEdit = QLineEdit() extraLayout.addWidget(self.prefixEdit) prefixLabel.setBuddy(self.prefixEdit) self.prefixEdit.textEdited.connect(self.mainDialogRef.setModified) extraLayout.addSpacing(8) suffixLabel = QLabel(_('Suffi&x')) extraLayout.addWidget(suffixLabel) self.suffixEdit = QLineEdit() extraLayout.addWidget(self.suffixEdit) suffixLabel.setBuddy(self.suffixEdit) self.suffixEdit.textEdited.connect(self.mainDialogRef.setModified) defaultBox = QGroupBox(_('Default &Value for New Nodes')) topLayout.addWidget(defaultBox, 2, 1) defaultLayout = QVBoxLayout(defaultBox) self.defaultCombo = QComboBox() defaultLayout.addWidget(self.defaultCombo) self.defaultCombo.setEditable(True) self.defaultCombo.editTextChanged.connect(self.mainDialogRef. setModified) self.heightBox = QGroupBox(_('Editor Height')) topLayout.addWidget(self.heightBox, 3, 1) heightLayout = QHBoxLayout(self.heightBox) heightLabel = QLabel(_('Num&ber of text lines')) heightLayout.addWidget(heightLabel) self.heightCtrl = QSpinBox() heightLayout.addWidget(self.heightCtrl) self.heightCtrl.setMinimum(1) self.heightCtrl.setMaximum(999) heightLabel.setBuddy(self.heightCtrl) self.heightCtrl.valueChanged.connect(self.mainDialogRef.setModified) self.equationBox = QGroupBox(_('Math Equation')) topLayout.addWidget(self.equationBox, 4, 0, 1, 2) equationLayout = QHBoxLayout(self.equationBox) self.equationViewer = QLineEdit() equationLayout.addWidget(self.equationViewer) self.equationViewer.setReadOnly(True) equationButton = QPushButton(_('Define Equation')) equationLayout.addWidget(equationButton) equationButton.clicked.connect(self.defineMathEquation) htmlBox = QGroupBox(_('Output HTML')) topLayout.addWidget(htmlBox, 5, 0) htmlLayout = QVBoxLayout(htmlBox) self.htmlButton = QCheckBox(_('Evaluate &HTML tags')) htmlLayout.addWidget(self.htmlButton) self.htmlButton.toggled.connect(self.mainDialogRef.setModified) topLayout.setRowStretch(6, 1) def updateContent(self): """Update page contents from current format settings. """ typeNames = ConfigDialog.formatsRef.typeNames() self.typeCombo.blockSignals(True) self.typeCombo.clear() self.typeCombo.addItems(typeNames) self.typeCombo.addItem(_fileInfoFormatName) if self.currentFileInfoField: self.typeCombo.setCurrentIndex(len(typeNames)) else: self.typeCombo.setCurrentIndex(typeNames.index(ConfigDialog. currentTypeName)) self.typeCombo.blockSignals(False) currentFormat, currentField = self.currentFormatAndField() 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 = fieldformat.fieldTypes.index(currentField.typeName) self.fieldTypeCombo.setCurrentIndex(selectNum) self.fieldTypeCombo.blockSignals(False) # also disable for generic types self.fieldTypeCombo.setEnabled(not self.currentFileInfoField) self.formatBox.setEnabled(currentField.defaultFormat != '') if (hasattr(currentField, 'resultType') and currentField.resultType == fieldformat.MathResult.text): self.formatBox.setEnabled(False) self.formatEdit.setText(currentField.format) self.prefixEdit.setText(currentField.prefix) self.suffixEdit.setText(currentField.suffix) self.defaultCombo.blockSignals(True) self.defaultCombo.clear() initDefault = currentField.getEditorInitDefault() self.defaultCombo.addItem(initDefault) initDefaultList = currentField.initDefaultChoices() if initDefaultList: if initDefaultList[0] == initDefault: initDefaultList[0] = '' # don't duplicate first entry self.defaultCombo.addItems(initDefaultList) self.defaultCombo.setCurrentIndex(0) self.defaultCombo.blockSignals(False) self.defaultCombo.setEnabled(currentField.supportsInitDefault and not self.currentFileInfoField) self.heightCtrl.blockSignals(True) self.heightCtrl.setValue(currentField.numLines) self.heightCtrl.blockSignals(False) self.heightBox.setEnabled(not self.currentFileInfoField and currentField.editorClassName in ('RichTextEditor', 'HtmlTextEditor', 'PlainTextEditor')) self.htmlButton.blockSignals(True) self.htmlButton.setChecked(currentField.evalHtml) self.htmlButton.blockSignals(False) self.htmlButton.setEnabled(not currentField.fixEvalHtmlSetting) if currentField.typeName == 'Math': self.equationBox.show() eqnText = currentField.equationText() self.equationViewer.setText(eqnText) else: self.equationBox.hide() def currentFormatAndField(self): """Return a tuple of the current format and field. Adjusts for a current file info field. """ if self.currentFileInfoField: currentFormat = ConfigDialog.formatsRef.fileInfoFormat fieldName = self.currentFileInfoField else: currentFormat = ConfigDialog.formatsRef[ConfigDialog. currentTypeName] fieldName = ConfigDialog.currentFieldName currentField = currentFormat.fieldDict[fieldName] return (currentFormat, currentField) def changeCurrentType(self, typeName): """Change the current format type based on a signal from lists. Arguments: typeName -- the name of the new current type """ self.readChanges() if typeName == _fileInfoFormatName: self.currentFileInfoField = (ConfigDialog.formatsRef. fileInfoFormat.fieldNames()[0]) else: ConfigDialog.currentTypeName = typeName ConfigDialog.currentFieldName = (ConfigDialog.formatsRef[typeName]. fieldNames()[0]) self.currentFileInfoField = '' self.updateContent() def changeCurrentField(self, fieldName): """Change the current format field based on a signal from lists. Arguments: fieldName -- the name of the new current field """ self.readChanges() if self.currentFileInfoField: self.currentFileInfoField = fieldName else: ConfigDialog.currentFieldName = fieldName self.updateContent() def changeFieldType(self): """Change the field type based on a combo box signal. """ self.readChanges() # preserve previous changes currentFormat, currentField = self.currentFormatAndField() selectNum = self.fieldTypeCombo.currentIndex() fieldTypeName = fieldformat.fieldTypes[selectNum] currentField.changeType(fieldTypeName) currentFormat.updateDerivedTypes() self.updateContent() self.mainDialogRef.setModified() def defineMathEquation(self): """Show the dialog to define an equation for a Math field. """ currentFormat, currentField = self.currentFormatAndField() prevEqnText = currentField.equationText() prevResultType = currentField.resultType dlg = MathEquationDialog(currentFormat, currentField, self) if (dlg.exec_() == QDialog.Accepted and (currentField.equationText() != prevEqnText or currentField.resultType != prevResultType)): self.mainDialogRef.setModified() self.updateContent() def formatHelp(self): """Provide a format help menu based on a button signal. """ currentFormat, currentField = self.currentFormatAndField() menu = QMenu(self) self.formatHelpDict = {} for descript, key in currentField.getFormatHelpMenuList(): if descript: self.formatHelpDict[descript] = key menu.addAction(descript) else: menu.addSeparator() menu.popup(self.helpButton. mapToGlobal(QPoint(0, self.helpButton.height()))) menu.triggered.connect(self.insertFormat) def insertFormat(self, action): """Insert format text from help menu into edit box. Arguments: action -- the action from the help menu """ self.formatEdit.insert(self.formatHelpDict[action.text()]) def readChanges(self): """Make changes to the format for each widget. """ currentFormat, currentField = self.currentFormatAndField() if not currentField.fixEvalHtmlSetting: currentField.evalHtml = self.htmlButton.isChecked() prevFormat = currentField.format try: currentField.setFormat(self.formatEdit.text()) if (self.currentFileInfoField and self.formatEdit.text() != prevFormat): currentFormat.fieldFormatModified = True except ValueError: self.formatEdit.setText(currentField.format) currentField.prefix = self.prefixEdit.text() currentField.suffix = self.suffixEdit.text() if self.currentFileInfoField and (currentField.prefix or currentField.suffix): currentFormat.fieldFormatModified = True try: currentField.setInitDefault(self.defaultCombo.currentText()) except ValueError: self.defaultCombo.blockSignals(True) self.defaultCombo.setEditText(currentField.getEditorInitDefault()) self.defaultCombo.blockSignals(False) currentField.numLines = self.heightCtrl.value() _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(r'{\*.*?\*}') class OutputPage(ConfigPage): """Config dialog page to define the node output strings. """ def __init__(self, parent=None): """Initialize the config dialog page. Arguments: parent -- the parent overall dialog """ super().__init__(parent) self.refLevelFlag = '' self.refLevelType = None topLayout = QGridLayout(self) typeBox = QGroupBox(_('&Data Type')) topLayout.addWidget(typeBox, 0, 0) typeLayout = QVBoxLayout(typeBox) self.typeCombo = QComboBox() typeLayout.addWidget(self.typeCombo) self.typeCombo.currentIndexChanged[str].connect(self.changeCurrentType) fieldBox = QGroupBox(_('F&ield List')) topLayout.addWidget(fieldBox, 1, 0, 2, 1) boxLayout = QVBoxLayout(fieldBox) self.fieldListBox = QTreeWidget() boxLayout.addWidget(self.fieldListBox) self.fieldListBox.setRootIsDecorated(False) self.fieldListBox.setSelectionMode(QAbstractItemView.ExtendedSelection) self.fieldListBox.setColumnCount(2) self.fieldListBox.setHeaderLabels([_('Name'), _('Type')]) self.fieldListBox.itemSelectionChanged.connect(self.changeField) titleButtonLayout = QVBoxLayout() topLayout.addLayout(titleButtonLayout, 1, 1) self.toTitleButton = QPushButton('>>') titleButtonLayout.addWidget(self.toTitleButton) self.toTitleButton.setMaximumWidth(self.toTitleButton. sizeHint().height()) self.toTitleButton.clicked.connect(self.fieldToTitle) self.delTitleButton = QPushButton('<<') titleButtonLayout.addWidget(self.delTitleButton) self.delTitleButton.setMaximumWidth(self.delTitleButton. sizeHint().height()) self.delTitleButton.clicked.connect(self.delTitleField) titleBox = QGroupBox(_('&Title Format')) topLayout.addWidget(titleBox, 1, 2) titleLayout = QVBoxLayout(titleBox) self.titleEdit = TitleEdit() titleLayout.addWidget(self.titleEdit) self.titleEdit.cursorPositionChanged.connect(self. setControlAvailability) self.titleEdit.textEdited.connect(self.mainDialogRef.setModified) outputButtonLayout = QVBoxLayout() topLayout.addLayout(outputButtonLayout, 2, 1) self.toOutputButton = QPushButton('>>') outputButtonLayout.addWidget(self.toOutputButton) self.toOutputButton.setMaximumWidth(self.toOutputButton. sizeHint().height()) self.toOutputButton.clicked.connect(self.fieldToOutput) self.delOutputButton = QPushButton('<<') outputButtonLayout.addWidget(self.delOutputButton) self.delOutputButton.setMaximumWidth(self.delOutputButton. sizeHint().height()) self.delOutputButton.clicked.connect(self.delOutputField) outputBox = QGroupBox(_('Out&put Format')) topLayout.addWidget(outputBox, 2, 2) outputLayout = QVBoxLayout(outputBox) self.outputEdit = QTextEdit() self.outputEdit.setLineWrapMode(QTextEdit.NoWrap) outputLayout.addWidget(self.outputEdit) self.outputEdit.setTabChangesFocus(True) self.outputEdit.cursorPositionChanged.connect(self. setControlAvailability) self.outputEdit.textChanged.connect(self.mainDialogRef.setModified) # advanced widgets otherBox = QGroupBox(_('Other Field References')) topLayout.addWidget(otherBox, 0, 2) self.advancedWidgets.append(otherBox) otherLayout = QHBoxLayout(otherBox) levelLayout = QVBoxLayout() otherLayout.addLayout(levelLayout) levelLayout.setSpacing(0) levelLabel = QLabel(_('Reference Le&vel')) levelLayout.addWidget(levelLabel) levelCombo = QComboBox() levelLayout.addWidget(levelCombo) levelLabel.setBuddy(levelCombo) levelCombo.addItems(_refLevelList) levelCombo.currentIndexChanged.connect(self.changeRefLevel) refTypeLayout = QVBoxLayout() otherLayout.addLayout(refTypeLayout) refTypeLayout.setSpacing(0) refTypeLabel = QLabel(_('Refere&nce Type')) refTypeLayout.addWidget(refTypeLabel) self.refTypeCombo = QComboBox() refTypeLayout.addWidget(self.refTypeCombo) refTypeLabel.setBuddy(self.refTypeCombo) self.refTypeCombo.currentIndexChanged.connect(self.changeRefType) topLayout.setRowStretch(1, 1) topLayout.setRowStretch(2, 1) def updateContent(self): """Update page contents from current format settings. """ typeNames = ConfigDialog.formatsRef.typeNames() self.typeCombo.blockSignals(True) self.typeCombo.clear() self.typeCombo.addItems(typeNames) self.typeCombo.setCurrentIndex(typeNames.index(ConfigDialog. currentTypeName)) self.typeCombo.blockSignals(False) currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] self.updateFieldList() self.titleEdit.blockSignals(True) self.titleEdit.setText(currentFormat.getTitleLine()) self.titleEdit.end(False) self.titleEdit.blockSignals(False) self.outputEdit.blockSignals(True) self.outputEdit.setPlainText('\n'.join(currentFormat.getOutputLines())) cursor = self.outputEdit.textCursor() cursor.movePosition(QTextCursor.End) self.outputEdit.setTextCursor(cursor) self.outputEdit.blockSignals(False) self.refTypeCombo.blockSignals(True) self.refTypeCombo.clear() self.refTypeCombo.addItems(typeNames) refLevelType = (self.refLevelType if self.refLevelType else ConfigDialog.currentTypeName) try: self.refTypeCombo.setCurrentIndex(typeNames.index(refLevelType)) except ValueError: # type no longer exists self.refLevelType = ConfigDialog.currentTypeName self.refTypeCombo.setCurrentIndex(typeNames.index(self. refLevelType)) self.refTypeCombo.blockSignals(False) self.setControlAvailability() def updateFieldList(self): """Reload the field list box. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] if not self.refLevelFlag: activeFormat = currentFormat elif self.refLevelFlag == '!': activeFormat = ConfigDialog.formatsRef.fileInfoFormat elif self.refLevelFlag == '#': activeFormat = nodeformat.DescendantCountFormat() else: try: activeFormat = ConfigDialog.formatsRef[self.refLevelType] except (KeyError, ValueError): self.refLevelType = ConfigDialog.currentTypeName activeFormat = currentFormat self.fieldListBox.blockSignals(True) self.fieldListBox.clear() for field in activeFormat.fields(): if field.showInDialog: typeName = fieldformat.translatedTypeName(field.typeName) QTreeWidgetItem(self.fieldListBox, [field.name, typeName]) selectList = self.fieldListBox.findItems(ConfigDialog.currentFieldName, Qt.MatchFixedString | Qt.MatchCaseSensitive) selectItem = (selectList[0] if selectList else self.fieldListBox.topLevelItem(0)) self.fieldListBox.setCurrentItem(selectItem) selectItem.setSelected(True) self.fieldListBox.setColumnWidth(0, self.fieldListBox.width() // 2) self.fieldListBox.blockSignals(False) def changeField(self): """Change the current format field based on a tree widget signal. Not set if a special field ref level is active. """ selectList = self.fieldListBox.selectedItems() if (not self.refLevelFlag and len(selectList) == 1): ConfigDialog.currentFieldName = selectList[0].text(0) self.setControlAvailability() def setControlAvailability(self): """Set controls available based on text cursor movements. """ fieldsSelected = len(self.fieldListBox.selectedItems()) > 0 cursorInTitleField = self.isCursorInTitleField() self.toTitleButton.setEnabled(cursorInTitleField == None and fieldsSelected) self.delTitleButton.setEnabled(cursorInTitleField == True) cursorInOutputField = self.isCursorInOutputField() self.toOutputButton.setEnabled(cursorInOutputField == None and fieldsSelected) self.delOutputButton.setEnabled(cursorInOutputField == True) self.refTypeCombo.setEnabled(self.refLevelFlag not in ('', '!', '#')) def fieldToTitle(self): """Add selected field to cursor pos in title editor. """ self.titleEdit.insert(self.selectedFieldSepNames(' ')) self.titleEdit.setFocus() def delTitleField(self): """Remove field from cursor pos in title editor. """ if self.isCursorInTitleField(True): self.titleEdit.insert('') def fieldToOutput(self): """Add selected field to cursor pos in output editor. """ self.outputEdit.insertPlainText(self.selectedFieldSepNames()) self.outputEdit.setFocus() def delOutputField(self): """Remove field from cursor pos in output editor. """ if self.isCursorInOutputField(True): self.outputEdit.insertPlainText('') def selectedFieldSepNames(self, sep='\n'): """Return selected field name(s) with proper separators. Adjusts for special field ref levels. Multiple selections result in fields joined with the separator. Arguments: sep -- the separator to join multiple fields. """ fields = ['{{*{0}{1}*}}'.format(self.refLevelFlag, item.text(0)) for item in self.fieldListBox.selectedItems()] return '\n'.join(fields) def isCursorInTitleField(self, selectField=False): """Return True if a field pattern encloses the cursor/selection. Return False if the selection overlaps a field. Return None if there is no field at the cursor. Arguments: selectField -- select the entire field pattern if True. """ cursorPos = self.titleEdit.cursorPosition() selectStart = self.titleEdit.selectionStart() if selectStart < 0: selectStart = cursorPos elif selectStart == cursorPos: # backward selection cursorPos += len(self.titleEdit.selectedText()) fieldPos = self.fieldPosAtCursor(selectStart, cursorPos, self.titleEdit.text()) if not fieldPos: return None start, end = fieldPos if start == None or end == None: return False if selectField: self.titleEdit.setSelection(start, end - start) return True def isCursorInOutputField(self, selectField=False): """Return True if a field pattern encloses the cursor/selection. Return False if the selection overlaps a field. Return None if there is no field at the cursor. Arguments: selectField -- select the entire field pattern if True. """ outputCursor = self.outputEdit.textCursor() selectStart = outputCursor.anchor() cursorPos = outputCursor.position() block = outputCursor.block() blockStart = block.position() if selectStart < blockStart or (selectStart > blockStart + block.length()): return False # multi-line selection fieldPos = self.fieldPosAtCursor(selectStart - blockStart, cursorPos - blockStart, block.text()) if not fieldPos: return None start, end = fieldPos if start == None or end == None: return False if selectField: outputCursor.setPosition(start + blockStart) outputCursor.setPosition(end + blockStart, QTextCursor.KeepAnchor) self.outputEdit.setTextCursor(outputCursor) return True def fieldPosAtCursor(self, anchorPos, cursorPos, textLine): """Find the position of the field pattern that encloses the selection. Return a tuple of (start, end) positions of the field if found. Return (start, None) or (None, end) if the selection overlaps. Return None if no field is found. Arguments: anchorPos -- the selection start cursorPos -- the selection end textLine -- the text to search """ for match in fieldPattern.finditer(textLine): start = (match.start() if match.start() < anchorPos < match.end() else None) end = (match.end() if match.start() < cursorPos < match.end() else None) if start != None or end != None: return (start, end) return None def changeRefLevel(self, num): """Change other field ref level based on a combobox signal. Arguments: num -- the combobox index selected """ self.refLevelFlag = _refLevelFlags[num] if self.refLevelFlag in ('', '!', '#'): self.refLevelType = None elif not self.refLevelType: self.refLevelType = ConfigDialog.currentTypeName self.updateFieldList() self.setControlAvailability() def changeRefType(self, num): """Change the other field ref level type based on a combobox signal. Arguments: num -- the combobox index selected """ self.refLevelType = ConfigDialog.formatsRef.typeNames()[num] self.updateFieldList() self.setControlAvailability() def readChanges(self): """Make changes to the format for each widget. """ currentFormat = ConfigDialog.formatsRef[ConfigDialog.currentTypeName] currentFormat.changeTitleLine(self.titleEdit.text()) currentFormat.changeOutputLines(self.outputEdit.toPlainText().strip(). split('\n'), not currentFormat.formatHtml) class TitleEdit(QLineEdit): """LineEdit that avoids changing the selection on focus changes. """ focusIn = pyqtSignal(QWidget) def __init__(self, parent=None): """Initialize the config dialog page. Arguments: parent -- the parent dialog """ super().__init__(parent) def focusInEvent(self, event): """Override to keep selection & cursor position. Arguments: event -- the focus event """ cursorPos = self.cursorPosition() selectStart = self.selectionStart() if selectStart == cursorPos: selectStart = cursorPos + len(self.selectedText()) super().focusInEvent(event) self.setCursorPosition(cursorPos) if selectStart >= 0: self.setSelection(selectStart, cursorPos - selectStart) self.focusIn.emit(self) def focusOutEvent(self, event): """Override to keep selection & cursor position. Arguments: event -- the focus event """ cursorPos = self.cursorPosition() selectStart = self.selectionStart() if selectStart == cursorPos: selectStart = cursorPos + len(self.selectedText()) super().focusOutEvent(event) self.setCursorPosition(cursorPos) if selectStart >= 0: self.setSelection(selectStart, cursorPos - selectStart) class TypeLimitCombo(QComboBox): """A combo box for selecting limited child types. """ limitChanged = pyqtSignal() def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.checkBoxDialog = None self.typeNames = [] self.selectSet = set() def updateLists(self, typeNames, selectSet): """Update control text and store data for popup. Arguments: typeNames -- a list of available type names selectSet -- a set of seleected type names """ self.typeNames = typeNames self.updateSelects(selectSet) def updateSelects(self, selectSet): """Update control text and store selected items. Arguments: selectSet -- a set of seleected type names """ self.selectSet = selectSet if not selectSet or selectSet == set(self.typeNames): text = _('[All Types Available]') self.selectSet = set() else: text = ', '.join(sorted(selectSet)) self.addItem(text) self.setCurrentText(text) def showPopup(self): """Override to show a popup entry widget in place of a list view. """ self.checkBoxDialog = TypeLimitCheckBox(self.typeNames, self.selectSet, self) self.checkBoxDialog.setMinimumWidth(self.width()) self.checkBoxDialog.buttonChanged.connect(self.updateFromButton) self.checkBoxDialog.show() pos = self.mapToGlobal(self.rect().bottomRight()) pos.setX(pos.x() - self.checkBoxDialog.width() + 1) screenBottom = (QApplication.desktop().screenGeometry(self). bottom()) if pos.y() + self.checkBoxDialog.height() > screenBottom: pos.setY(pos.y() - self.rect().height() - self.checkBoxDialog.height()) self.checkBoxDialog.move(pos) def hidePopup(self): """Override to hide the popup entry widget. """ if self.checkBoxDialog: self.checkBoxDialog.hide() super().hidePopup() def updateFromButton(self): """Update selected items based on a button change. """ self.updateSelects(self.checkBoxDialog.selectSet()) self.limitChanged.emit() class TypeLimitCheckBox(QDialog): """A popup dialog box for selecting limited child types. """ buttonChanged = pyqtSignal() def __init__(self, textList, selectSet, parent=None): """Initialize the combination dialog. Arguments: textList -- a list of text choices selectSet -- a set of choices to preselect parent -- the parent, if given """ super().__init__(parent) self.setWindowFlags(Qt.Popup) topLayout = QVBoxLayout(self) topLayout.setContentsMargins(0, 0, 0, 0) scrollArea = QScrollArea() scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) topLayout.addWidget(scrollArea) innerWidget = QWidget() innerLayout = QVBoxLayout(innerWidget) self.buttonGroup = QButtonGroup(self) self.buttonGroup.setExclusive(False) self.buttonGroup.buttonClicked.connect(self.buttonChanged) for text in textList: button = QCheckBox(text, innerWidget) if text in selectSet: button.setChecked(True) self.buttonGroup.addButton(button) innerLayout.addWidget(button) scrollArea.setWidget(innerWidget) buttons = self.buttonGroup.buttons() if buttons: buttons[0].setFocus() def selectSet(self): """Return a set of currently checked text. """ result = set() for button in self.buttonGroup.buttons(): if button.isChecked(): result.add(button.text()) return result def selectAll(self): """Select all of the entries. """ for button in self.buttonGroup.buttons(): button.setChecked(True) def selectNone(self): """Clear all of the selections. """ for button in self.buttonGroup.buttons(): button.setChecked(False) def contextMenuEvent(self, event): """Create a popup context menu. Arguments: event -- the menu even to process """ menu = QMenu(self) menu.addAction(_('&Select All'), self.selectAll) menu.addAction(_('Select &None'), self.selectNone) menu.exec_(event.globalPos()) _illegalRe = re.compile(r'[^\w_\-.]') class NameEntryDialog(QDialog): """Dialog to handle user entry of a type or field name. Restricts entry to alpha-numerics, underscores, dashes and periods. """ def __init__(self, caption, labelText, defaultText='', addCheckBox = '', badText=None, parent=None): """Initialize the name entry class. Arguments: caption -- the window title labelText -- text for a descriptive lable defaultText -- initial text addCheckBox -- the label for an extra check box if needed badText -- a set or list of other illegal strings parent -- the parent overall dialog """ super().__init__(parent) self.badText = set() if badText: self.badText = badText self.text = '' self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(caption) topLayout = QVBoxLayout(self) label = QLabel(labelText) topLayout.addWidget(label) self.entry = QLineEdit(defaultText) topLayout.addWidget(self.entry) self.entry.setFocus() self.entry.returnPressed.connect(self.accept) self.extraChecked = False if addCheckBox: self.extraCheckBox = QCheckBox(addCheckBox) topLayout.addWidget(self.extraCheckBox) else: self.extraCheckBox = None ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch() okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) def accept(self): """Check for acceptable string before closing. """ self.text = self.entry.text().strip() if not self.text: error = _('The name cannot be empty') elif not self.text[0].isalpha(): error = _('The name must start with a letter') elif self.text[:3].lower() == 'xml': error = _('The name cannot start with "xml"') elif ' ' in self.text: error = _('The name cannot contain spaces') elif _illegalRe.search(self.text): badChars = set(_illegalRe.findall(self.text)) error = (_('The following characters are not allowed: {}'). format(''.join(badChars))) elif self.text in self.badText: error = _('The name was already used') else: if self.extraCheckBox: self.extraChecked = self.extraCheckBox.isChecked() return super().accept() QMessageBox.warning(self, 'TreeLine', error) class IconSelectDialog(QDialog): """Dialog for selecting icons for a format type. """ dialogSize = () dialogPos = () def __init__(self, nodeFormat, parent=None): """Create the icon select dialog. Arguments: nodeFormat -- the current node format to be set parent -- the parent overall dialog """ super().__init__(parent) self.currentIconName = nodeFormat.iconName if (not self.currentIconName or self.currentIconName not in globalref.treeIcons): self.currentIconName = icondict.defaultName self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Set Data Type Icon')) topLayout = QVBoxLayout(self) self.iconView = QListWidget() self.iconView.setViewMode(QListView.ListMode) self.iconView.setMovement(QListView.Static) self.iconView.setWrapping(True) self.iconView.setGridSize(QSize(112, 32)) topLayout.addWidget(self.iconView) self.iconView.itemDoubleClicked.connect(self.accept) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch() clearButton = QPushButton(_('Clear &Select')) ctrlLayout.addWidget(clearButton) clearButton.clicked.connect(self.iconView.clearSelection) okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) if IconSelectDialog.dialogSize: self.resize(IconSelectDialog.dialogSize[0], IconSelectDialog.dialogSize[1]) self.move(IconSelectDialog.dialogPos[0], IconSelectDialog.dialogPos[1]) self.loadIcons() def loadIcons(self): """Load icons from the icon dict source. """ if not globalref.treeIcons.allLoaded: globalref.treeIcons.loadAllIcons() for name, icon in globalref.treeIcons.items(): if icon: item = QListWidgetItem(icon, name, self.iconView) if name == self.currentIconName: self.iconView.setCurrentItem(item) self.iconView.sortItems() selectedItem = self.iconView.currentItem() if selectedItem: self.iconView.scrollToItem(selectedItem, QAbstractItemView.PositionAtCenter) def saveSize(self): """Record dialog size at close. """ IconSelectDialog.dialogSize = (self.width(), self.height()) IconSelectDialog.dialogPos = (self.x(), self.y()) def accept(self): """Save changes before closing. """ selectedItems = self.iconView.selectedItems() if selectedItems: self.currentIconName = selectedItems[0].text() if self.currentIconName == icondict.defaultName: self.currentIconName = '' else: self.currentIconName = icondict.noneName self.saveSize() super().accept() def reject(self): """Save dialog size before closing. """ self.saveSize() super().reject() class SortKeyDialog(QDialog): """Dialog for defining sort key fields and directions. """ directionNameDict = {True: _('forward'), False: _('reverse')} directionVarDict = dict([(name, boolVal) for boolVal, name in directionNameDict.items()]) def __init__(self, fieldDict, parent=None): """Create the sort key dialog. Arguments: fieldDict -- a dict of field names and values parent -- the parent overall dialog """ super().__init__(parent) self.fieldDict = fieldDict self.numChanges = 0 self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Sort Key Fields')) topLayout = QVBoxLayout(self) horizLayout = QHBoxLayout() topLayout.addLayout(horizLayout) fieldBox = QGroupBox(_('Available &Fields')) horizLayout.addWidget(fieldBox) boxLayout = QVBoxLayout(fieldBox) self.fieldListBox = QTreeWidget() boxLayout.addWidget(self.fieldListBox) self.fieldListBox.setRootIsDecorated(False) self.fieldListBox.setColumnCount(2) self.fieldListBox.setHeaderLabels([_('Name'), _('Type')]) midButtonLayout = QVBoxLayout() horizLayout.addLayout(midButtonLayout) self.addFieldButton = QPushButton('>>') midButtonLayout.addWidget(self.addFieldButton) self.addFieldButton.setMaximumWidth(self.addFieldButton. sizeHint().height()) self.addFieldButton.clicked.connect(self.addField) self.removeFieldButton = QPushButton('<<') midButtonLayout.addWidget(self.removeFieldButton) self.removeFieldButton.setMaximumWidth(self.removeFieldButton. sizeHint().height()) self.removeFieldButton.clicked.connect(self.removeField) sortBox = QGroupBox(_('&Sort Criteria')) horizLayout.addWidget(sortBox) boxLayout = QVBoxLayout(sortBox) self.sortListBox = QTreeWidget() boxLayout.addWidget(self.sortListBox) self.sortListBox.setRootIsDecorated(False) self.sortListBox.setColumnCount(3) self.sortListBox.setHeaderLabels(['#', _('Field'), _('Direction')]) self.sortListBox.currentItemChanged.connect(self.setControlsAvail) rightButtonLayout = QVBoxLayout() horizLayout.addLayout(rightButtonLayout) self.upButton = QPushButton(_('Move &Up')) rightButtonLayout.addWidget(self.upButton) self.upButton.clicked.connect(self.moveUp) self.downButton = QPushButton(_('&Move Down')) rightButtonLayout.addWidget(self.downButton) self.downButton.clicked.connect(self.moveDown) self.flipButton = QPushButton(_('Flip &Direction')) rightButtonLayout.addWidget(self.flipButton) self.flipButton.clicked.connect(self.flipDirection) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch() self.okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.updateContent() def updateContent(self): """Update dialog contents from current format settings. """ sortFields = [field for field in self.fieldDict.values() if field.sortKeyNum > 0] sortFields.sort(key = operator.attrgetter('sortKeyNum')) if not sortFields: sortFields = [list(self.fieldDict.values())[0]] self.fieldListBox.clear() for field in self.fieldDict.values(): if field not in sortFields: QTreeWidgetItem(self.fieldListBox, [field.name, field.typeName]) if self.fieldListBox.topLevelItemCount() > 0: self.fieldListBox.setCurrentItem(self.fieldListBox.topLevelItem(0)) self.fieldListBox.setColumnWidth(0, self.fieldListBox.sizeHint(). width() // 2) self.sortListBox.blockSignals(True) self.sortListBox.clear() for num, field in enumerate(sortFields, 1): sortDir = SortKeyDialog.directionNameDict[field.sortKeyForward] QTreeWidgetItem(self.sortListBox, [repr(num), field.name, sortDir]) self.sortListBox.setCurrentItem(self.sortListBox.topLevelItem(0)) self.sortListBox.blockSignals(False) self.sortListBox.setColumnWidth(0, self.sortListBox.sizeHint(). width() // 8) self.setControlsAvail() def setControlsAvail(self): """Set controls available based on selections. """ self.addFieldButton.setEnabled(self.fieldListBox.topLevelItemCount() > 0) hasSortItems = self.sortListBox.topLevelItemCount() > 0 self.removeFieldButton.setEnabled(hasSortItems) if hasSortItems: sortPosNum = self.sortListBox.indexOfTopLevelItem(self.sortListBox. currentItem()) self.upButton.setEnabled(hasSortItems and sortPosNum > 0) self.downButton.setEnabled(hasSortItems and sortPosNum < self.sortListBox.topLevelItemCount() - 1) self.flipButton.setEnabled(hasSortItems) self.okButton.setEnabled(hasSortItems) def addField(self): """Add a field to the sort criteria list. """ itemNum = self.fieldListBox.indexOfTopLevelItem(self.fieldListBox. currentItem()) fieldName = self.fieldListBox.takeTopLevelItem(itemNum).text(0) field = self.fieldDict[fieldName] sortNum = self.sortListBox.topLevelItemCount() + 1 sortDir = SortKeyDialog.directionNameDict[field.sortKeyForward] self.sortListBox.blockSignals(True) sortItem = QTreeWidgetItem(self.sortListBox, [repr(sortNum), fieldName, sortDir]) self.sortListBox.setCurrentItem(sortItem) self.sortListBox.blockSignals(False) self.setControlsAvail() self.numChanges += 1 def removeField(self): """Remove a field from the sort criteria list. """ itemNum = self.sortListBox.indexOfTopLevelItem(self.sortListBox. currentItem()) self.sortListBox.blockSignals(True) fieldName = self.sortListBox.takeTopLevelItem(itemNum).text(1) self.renumberSortFields() self.sortListBox.blockSignals(False) field = self.fieldDict[fieldName] sortFieldNames = set() for num in range(self.sortListBox.topLevelItemCount()): sortFieldNames.add(self.sortListBox.topLevelItem(num).text(1)) fieldList = [field for field in self.fieldDict.values() if field.name not in sortFieldNames] pos = fieldList.index(field) fieldItem = QTreeWidgetItem([fieldName, field.typeName]) self.fieldListBox.insertTopLevelItem(pos, fieldItem) self.setControlsAvail() self.numChanges += 1 def moveUp(self): """Move a field upward in the sort criteria. """ itemNum = self.sortListBox.indexOfTopLevelItem(self.sortListBox. currentItem()) self.sortListBox.blockSignals(True) sortItem = self.sortListBox.takeTopLevelItem(itemNum) self.sortListBox.insertTopLevelItem(itemNum - 1, sortItem) self.sortListBox.setCurrentItem(sortItem) self.renumberSortFields() self.sortListBox.blockSignals(False) self.setControlsAvail() self.numChanges += 1 def moveDown(self): """Move a field downward in the sort criteria. """ itemNum = self.sortListBox.indexOfTopLevelItem(self.sortListBox. currentItem()) self.sortListBox.blockSignals(True) sortItem = self.sortListBox.takeTopLevelItem(itemNum) self.sortListBox.insertTopLevelItem(itemNum + 1, sortItem) self.sortListBox.setCurrentItem(sortItem) self.renumberSortFields() self.sortListBox.blockSignals(False) self.setControlsAvail() self.numChanges += 1 def flipDirection(self): """Toggle the direction of the current sort field. """ sortItem = self.sortListBox.currentItem() oldDirection = SortKeyDialog.directionVarDict[sortItem.text(2)] newDirection = SortKeyDialog.directionNameDict[not oldDirection] sortItem.setText(2, newDirection) self.numChanges += 1 def renumberSortFields(self): """Update field numbers in the sort list. """ for num in range(self.sortListBox.topLevelItemCount()): self.sortListBox.topLevelItem(num).setText(0, repr(num + 1)) def accept(self): """Save changes before closing. """ if not self.numChanges: return self.reject() for field in self.fieldDict.values(): field.sortKeyNum = 0 field.sortKeyForward = True for num in range(self.sortListBox.topLevelItemCount()): fieldItem = self.sortListBox.topLevelItem(num) field = self.fieldDict[fieldItem.text(1)] field.sortKeyNum = num + 1 field.sortKeyForward = SortKeyDialog.directionVarDict[fieldItem. text(2)] return super().accept() _mathRefLevels = [_('Self Reference'), _('Parent Reference'), _('Root Reference'), _('Child Reference'), _('Child Count')] # _mathRefLevelFlags correspond to _mathRefLevels _mathRefLevelFlags = ['', '*', '$', '&', '#'] _mathResultTypes = [N_('Number Result'), N_('Date Result'), N_('Time Result'), N_('Boolean Result'), N_('Text Result')] _operatorTypes = [_('Arithmetic Operators'), _('Comparison Operators'), _('Text Operators')] _arithmeticOperators = [('+', _('add')), ('-', _('subtract')), ('*', _('multiply')), ('/', _('divide')), ('//', _('floor divide')), ('%', _('modulus')), ('**', _('power')), ('sum()', _('sum of items')), ('max()', _('maximum')), ('min()', _('minimum')), ('mean()', _('average')), ('abs()', _('absolute value')), ('sqrt()', _('square root')), ('log()', _('natural logarithm')), ('log10()', _('base-10 logarithm')), ('factorial()', _('factorial')), ('round()', _('round to num digits')), ('floor()', _('lower integer')), ('ceil()', _('higher integer')), ('int()', _('truncated integer')), ('float()', _('floating point')), ('sin()', _('sine of radians')), ('cos()', _('cosine of radians')), ('tan()', _('tangent of radians')), ('asin()', _('arc sine')), ('acos()', _('arc cosine')), ('atan()', _('arc tangent')), ('degrees()', _('radians to degrees')), ('radians()', _('degrees to radians')), ('pi', _('pi constant')), ('e', _('natural log constant'))] _comparisonOperators = [('==', _('equal to')), ('<', _('less than')), ('>', _('greater than')), ('<=', _('less than or equal to')), ('>=', _('greater than or equal to')), ('!=', _('not equal to')), ('() if () else ()', _('true value, condition, false value')), ('and', _('logical and')), ('or', _('logical or')), ('startswith()', _('true if 1st text arg starts with 2nd arg')), ('endswith()', _('true if 1st text arg ends with 2nd arg')), ('contains()', _('true if 1st text arg contains 2nd arg'))] _textOperators = [('+', _('concatenate text')), ("join(' ', )", _('join text using 1st arg as separator')), ('upper()', _('convert text to upper case')), ('lower()', _('convert text to lower case')), ('replace()', _('in 1st arg, replace 2nd arg with 3rd arg'))] # _operatorLists correspond to _operatorTypes _operatorLists = [_arithmeticOperators, _comparisonOperators, _textOperators] class MathEquationDialog(QDialog): """Dialog for defining equations for Math fields. """ def __init__(self, nodeFormat, field, parent=None): """Create the math equation dialog. Arguments: nodeFormat -- the current node format field -- the Math field """ super().__init__(parent) self.nodeFormat = nodeFormat self.typeFormats = self.nodeFormat.parentFormats self.field = field self.refLevelFlag = '' self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Define Math Field Equation')) topLayout = QGridLayout(self) fieldBox = QGroupBox(_('Field References')) topLayout.addWidget(fieldBox, 0, 0, 2, 1) fieldLayout = QVBoxLayout(fieldBox) innerLayout = QVBoxLayout() innerLayout.setSpacing(0) fieldLayout.addLayout(innerLayout) levelLabel = QLabel(_('Reference &Level')) innerLayout.addWidget(levelLabel) levelCombo = QComboBox() innerLayout.addWidget(levelCombo) levelLabel.setBuddy(levelCombo) levelCombo.addItems(_mathRefLevels) levelCombo.currentIndexChanged.connect(self.changeRefLevel) innerLayout = QVBoxLayout() innerLayout.setSpacing(0) fieldLayout.addLayout(innerLayout) typeLabel = QLabel(_('Reference &Type')) innerLayout.addWidget(typeLabel) self.typeCombo = QComboBox() innerLayout.addWidget(self.typeCombo) typeLabel.setBuddy(self.typeCombo) self.typeCombo.addItems(self.typeFormats.typeNames()) self.typeCombo.currentIndexChanged.connect(self.updateFieldList) innerLayout = QVBoxLayout() innerLayout.setSpacing(0) fieldLayout.addLayout(innerLayout) fieldLabel = QLabel(_('Available &Field List')) innerLayout.addWidget(fieldLabel) self.fieldListBox = QTreeWidget() innerLayout.addWidget(self.fieldListBox) fieldLabel.setBuddy(self.fieldListBox) self.fieldListBox.setRootIsDecorated(False) self.fieldListBox.setColumnCount(2) self.fieldListBox.setHeaderLabels([_('Name'), _('Type')]) resultTypeBox = QGroupBox(_('&Result Type')) topLayout.addWidget(resultTypeBox, 0, 1) resultTypeLayout = QVBoxLayout(resultTypeBox) self.resultTypeCombo = QComboBox() resultTypeLayout.addWidget(self.resultTypeCombo) self.resultTypeCombo.addItems([_(str) for str in _mathResultTypes]) results = [s.split(' ', 1)[0].lower() for s in _mathResultTypes] resultStr = self.field.resultType.name self.resultTypeCombo.setCurrentIndex(results.index(resultStr)) operBox = QGroupBox(_('Operations')) topLayout.addWidget(operBox, 1, 1) operLayout = QVBoxLayout(operBox) innerLayout = QVBoxLayout() innerLayout.setSpacing(0) operLayout.addLayout(innerLayout) operTypeLabel = QLabel(_('O&perator Type')) innerLayout.addWidget(operTypeLabel) operTypeCombo = QComboBox() innerLayout.addWidget(operTypeCombo) operTypeLabel.setBuddy(operTypeCombo) operTypeCombo.addItems(_operatorTypes) operTypeCombo.currentIndexChanged.connect(self.replaceOperatorList) innerLayout = QVBoxLayout() innerLayout.setSpacing(0) operLayout.addLayout(innerLayout) operListLabel = QLabel(_('Oper&ator List')) innerLayout.addWidget(operListLabel) self.operListBox = QTreeWidget() innerLayout.addWidget(self.operListBox) operListLabel.setBuddy(self.operListBox) self.operListBox.setRootIsDecorated(False) self.operListBox.setColumnCount(2) self.operListBox.setHeaderLabels([_('Name'), _('Description')]) self.replaceOperatorList(0) buttonLayout = QHBoxLayout() topLayout.addLayout(buttonLayout, 2, 0) buttonLayout.addStretch() self.addFieldButton = QPushButton('\u25bc') buttonLayout.addWidget(self.addFieldButton) self.addFieldButton.setMaximumWidth(self.addFieldButton. sizeHint().height()) self.addFieldButton.clicked.connect(self.addField) self.delFieldButton = QPushButton('\u25b2') buttonLayout.addWidget(self.delFieldButton) self.delFieldButton.setMaximumWidth(self.delFieldButton. sizeHint().height()) self.delFieldButton.clicked.connect(self.deleteField) buttonLayout.addStretch() buttonLayout = QHBoxLayout() topLayout.addLayout(buttonLayout, 2, 1) self.addOperButton = QPushButton('\u25bc') buttonLayout.addWidget(self.addOperButton) self.addOperButton.setMaximumWidth(self.addOperButton. sizeHint().height()) self.addOperButton.clicked.connect(self.addOperator) equationBox = QGroupBox(_('&Equation')) topLayout.addWidget(equationBox, 3, 0, 1, 2) equationLayout = QVBoxLayout(equationBox) self.equationEdit = TitleEdit() equationLayout.addWidget(self.equationEdit) self.equationEdit.setText(self.field.equationText()) self.equationEdit.cursorPositionChanged.connect(self. setControlAvailability) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout, 4, 0, 1, 2) ctrlLayout.addStretch() okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.setDefault(True) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.changeRefLevel(0) self.equationEdit.setFocus() def updateFieldList(self): """Update field list based on reference type setting. """ currentFormat = self.typeFormats[self.typeCombo.currentText()] self.fieldListBox.clear() if self.refLevelFlag != '#': for field in currentFormat.fields(): if (hasattr(field, 'mathValue') and field.showInDialog and (self.refLevelFlag or field != self.field)): QTreeWidgetItem(self.fieldListBox, [field.name, _(field.typeName)]) else: QTreeWidgetItem(self.fieldListBox, [_('Count'), _('Number of Children')]) if self.fieldListBox.topLevelItemCount(): selectItem = self.fieldListBox.topLevelItem(0) self.fieldListBox.setCurrentItem(selectItem) selectItem.setSelected(True) self.fieldListBox.resizeColumnToContents(0) self.fieldListBox.setColumnWidth(0, self.fieldListBox.columnWidth(0) * 2) self.setControlAvailability() def setControlAvailability(self): """Set controls available based on text cursor movements. """ cursorInField = self.isCursorInField() fieldCount = self.fieldListBox.topLevelItemCount() self.addFieldButton.setEnabled(cursorInField == None and fieldCount) self.delFieldButton.setEnabled(cursorInField == True) self.addOperButton.setEnabled(cursorInField == None) def addField(self): """Add selected field to cursor pos in the equation editor. """ fieldSepName = '{{*{0}{1}*}}'.format(self.refLevelFlag, self.fieldListBox.currentItem(). text(0)) self.equationEdit.insert(fieldSepName) self.equationEdit.setFocus() def deleteField(self): """Remove field from cursor pos in the equation editor. """ if self.isCursorInField(True): self.equationEdit.insert('') self.equationEdit.setFocus() def addOperator(self): """Add selected operator to cursor pos in the equation editor. """ oper = self.operListBox.currentItem().text(0) origText = self.equationEdit.text() cursorPos = self.equationEdit.cursorPosition() if cursorPos != 0 and origText[cursorPos - 1] != ' ': oper = ' ' + oper self.equationEdit.insert(oper + ' ') parenPos = oper.find(')') if parenPos >= 0: cursorPos = self.equationEdit.cursorPosition() self.equationEdit.setCursorPosition(cursorPos - len(oper) + parenPos - 1) self.equationEdit.setFocus() def isCursorInField(self, selectField=False): """Return True if a field pattern encloses the cursor/selection. Return False if the selection overlaps a field. Return None if there is no field at the cursor. Arguments: selectField -- select the entire field pattern if True. """ cursorPos = self.equationEdit.cursorPosition() selectStart = self.equationEdit.selectionStart() if selectStart < 0: selectStart = cursorPos elif selectStart == cursorPos: # backward selection cursorPos += len(self.equationEdit.selectedText()) start = end = None for match in fieldPattern.finditer(self.equationEdit.text()): start = (match.start() if match.start() < selectStart < match.end() else None) end = (match.end() if match.start() < cursorPos < match.end() else None) if start != None or end != None: break if start == None and end == None: return None if start == None or end == None: return False if selectField: self.equationEdit.setSelection(start, end - start) return True def changeRefLevel(self, num): """Change the reference level based on a combobox signal. Arguments: num -- the combobox index selected """ self.refLevelFlag = _mathRefLevelFlags[num] if self.refLevelFlag in ('', '#'): self.typeCombo.setEnabled(False) self.typeCombo.setCurrentIndex(self.typeFormats.typeNames(). index(self.nodeFormat.name)) else: self.typeCombo.setEnabled(True) self.updateFieldList() def replaceOperatorList(self, num): """Change the operator list based on a signal from the oper type combo. Arguments: num -- the combobox index selected """ self.operListBox.clear() for oper, descr in _operatorLists[num]: QTreeWidgetItem(self.operListBox, [oper, descr]) self.operListBox.resizeColumnToContents(0) self.operListBox.setColumnWidth(0, int(self.operListBox.columnWidth(0) * 1.2)) self.operListBox.resizeColumnToContents(1) selectItem = self.operListBox.topLevelItem(0) self.operListBox.setCurrentItem(selectItem) selectItem.setSelected(True) def accept(self): """Verify the equation and close the dialog if acceptable. """ eqnText = self.equationEdit.text().strip() if eqnText: eqn = matheval.MathEquation(eqnText) try: eqn.validate() except ValueError as err: QMessageBox.warning(self, 'TreeLine', _('Equation error: {}').format(err)) return self.typeFormats.emptiedMathDict.setdefault(self.nodeFormat.name, set()).discard(self.field.name) self.field.equation = eqn else: if self.field.equationText(): self.typeFormats.emptiedMathDict.setdefault(self.nodeFormat. name, set()).add(self.field.name) self.field.equation = None resultStr = (_mathResultTypes[self.resultTypeCombo.currentIndex()]. split(' ', 1)[0].lower()) self.field.changeResultType(fieldformat.MathResult[resultStr]) super().accept() TreeLine/source/dataeditview.py0000644000175000017500000010575513646200106015602 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # dataeditview.py, provides a class for the data edit right-hand view # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** from PyQt5.QtCore import QEvent, QPointF, QRectF, QSize, Qt, pyqtSignal from PyQt5.QtGui import (QKeySequence, QPainterPath, QPalette, QPen, QSyntaxHighlighter, QTextCharFormat, QTextCursor, QTextDocument) from PyQt5.QtWidgets import (QAbstractItemView, QApplication, QStyledItemDelegate, QTableWidget, QTableWidgetItem) import treenode import undo import urltools import dataeditors import globalref _minColumnWidth = 80 defaultFont = None class DataEditCell(QTableWidgetItem): """Class override for data edit view cells. Used for the cells with editable content. """ def __init__(self, spot, field, titleCellRef, typeCellRef): """Initialize the editable cells in the data edit view. Arguments: spot -- the spot referenced by this cell field -- the field object referenced by this cell titleCellRef -- the title cell to update based on data changes typeCellRef -- the format type cell to update based on type changes """ super().__init__() self.spot = spot self.node = spot.nodeRef self.field = field self.titleCellRef = titleCellRef self.typeCellRef = typeCellRef self.errorFlag = False self.cursorPos = (-1, -1) self.scrollPos = -1 # store doc to speed up delegate sizeHint and paint calls self.doc = QTextDocument() self.doc.setDefaultFont(defaultFont) self.doc.setDocumentMargin(6) self.updateText() def updateText(self): """Update the text based on the current node data. """ self.errorFlag = False try: self.setText(self.field.editorText(self.node)) except ValueError as err: if len(err.args) >= 2: self.setText(err.args[1]) else: self.setText(self.node.data.get(self.field.name, '')) self.errorFlag = True if self.field.showRichTextInCell: self.doc.setHtml(self.text()) else: self.doc.setPlainText(self.text()) def storeEditorState(self, editor): """Store the cursor & scroll positions baseed on an editor signal. Arguments: editor -- the editor that will get its state saved """ self.cursorPos = editor.cursorPosTuple() self.scrollPos = editor.scrollPosition() class DataEditDelegate(QStyledItemDelegate): """Class override for display and editing of DataEditCells. """ def __init__(self, parent=None): """Initialize the delegate class. Arguments: parent -- the parent view """ super().__init__(parent) self.editorClickPos = None self.tallEditScrollPos = -1 self.lastEditor = None self.prevNumLines = -1 def paint(self, painter, styleOption, modelIndex): """Paint the Data Edit Cells with support for rich text. Other cells are painted with the base class default. Also paints an error rectangle if the format error flag is set. Arguments: painter -- the painter instance styleOption -- the data for styles and geometry modelIndex -- the index of the cell to be painted """ cell = self.parent().item(modelIndex.row(), modelIndex.column()) if isinstance(cell, DataEditCell): painter.save() doc = cell.doc doc.setTextWidth(styleOption.rect.width()) painter.translate(styleOption.rect.topLeft()) paintRect = QRectF(0, 0, styleOption.rect.width(), styleOption.rect.height()) painter.setClipRect(paintRect) painter.fillRect(paintRect, QApplication.palette().base()) painter.setPen(QPen(QApplication.palette().text(), 1)) painter.drawRect(paintRect.adjusted(0, 0, -1, -1)) doc.drawContents(painter) if cell.errorFlag: path = QPainterPath(QPointF(0, 0)) path.lineTo(0, 10) path.lineTo(10, 0) path.closeSubpath() painter.fillPath(path, QApplication.palette().highlight()) painter.restore() else: super().paint(painter, styleOption, modelIndex) def sizeHint(self, styleOption, modelIndex): """Return the size of Data Edit Cells with rich text. Other cells return the base class size. Arguments: styleOption -- the data for styles and geometry modelIndex -- the index of the cell to be painted """ cell = self.parent().item(modelIndex.row(), modelIndex.column()) if isinstance(cell, DataEditCell): doc = cell.doc doc.setTextWidth(styleOption.rect.width()) size = doc.documentLayout().documentSize().toSize() maxHeight = self.parent().height() * 9 // 10 # 90% of view height if (size.height() > maxHeight and globalref.genOptions['EditorLimitHeight']): size.setHeight(maxHeight) if cell.field.numLines > 1: minDoc = QTextDocument('\n' * (cell.field.numLines - 1)) minDoc.setDefaultFont(cell.doc.defaultFont()) minHeight = (minDoc.documentLayout().documentSize().toSize(). height()) if minHeight > size.height(): size.setHeight(minHeight) return size + QSize(0, 4) return super().sizeHint(styleOption, modelIndex) def createEditor(self, parent, styleOption, modelIndex): """Return a new text editor for a cell. Arguments: parent -- the parent widget for the editor styleOption -- the data for styles and geometry modelIndex -- the index of the cell to be edited """ cell = self.parent().item(modelIndex.row(), modelIndex.column()) if isinstance(cell, DataEditCell): editor = getattr(dataeditors, cell.field.editorClassName)(parent) editor.setFont(cell.doc.defaultFont()) if hasattr(editor, 'fieldRef'): editor.fieldRef = cell.field if hasattr(editor, 'nodeRef'): editor.nodeRef = cell.node if cell.errorFlag: editor.setErrorFlag() # self.parent().setFocusProxy(editor) editor.contentsChanged.connect(self.commitData) editor.editEnding.connect(cell.storeEditorState) if (not globalref.genOptions['EditorLimitHeight'] and hasattr(editor, 'keyPressed')): editor.keyPressed.connect(self.scrollOnKeyPress) if hasattr(editor, 'inLinkSelectMode'): editor.inLinkSelectMode.connect(self.parent(). changeInLinkSelectMode) if hasattr(editor, 'setLinkFromNode'): self.parent().internalLinkSelected.connect(editor. setLinkFromNode) # viewport filter required to catch editor events try: editor.viewport().installEventFilter(self) except AttributeError: try: editor.lineEdit().installEventFilter(self) except AttributeError: pass self.lastEditor = editor editor.setFocus() return editor return super().createEditor(parent, styleOption, modelIndex) def setEditorData(self, editor, modelIndex): """Sets the text to be edited by the editor item. Arguments: editor -- the editor widget modelIndex -- the index of the cell to being edited """ cell = self.parent().item(modelIndex.row(), modelIndex.column()) if isinstance(cell, DataEditCell): try: # set from data to pick up any background changes editor.setContents(cell.field.editorText(cell.node)) except ValueError: # if data bad, just set it like the cell editor.setContents(modelIndex.data()) if cell.errorFlag: editor.setErrorFlag() editor.show() if self.editorClickPos: editor.setCursorPoint(self.editorClickPos) self.editorClickPos = None elif globalref.genOptions['EditorLimitHeight']: if cell.cursorPos[1] >= 0: editor.setCursorPos(*cell.cursorPos) cell.cursorPos = (-1, -1) if cell.scrollPos >= 0: editor.setScrollPosition(cell.scrollPos) cell.scrollPos = -1 else: editor.resetCursor() if (not globalref.genOptions['EditorLimitHeight'] and globalref.genOptions['EditorOnHover'] and self.tallEditScrollPos >= 0): # maintain scroll position for unlimited height editors # when hovering (use adjustScroll() for non-hovering self.parent().verticalScrollBar().setValue(self. tallEditScrollPos) else: super().setEditorData(editor, modelIndex) def setModelData(self, editor, styleOption, modelIndex): """Update the model with the results from an editor. Sets the cell error flag if the format doesn't match. Arguments: editor -- the editor widget styleOption -- the data for styles and geometry modelIndex -- the index of the cell to be painted """ cell = self.parent().item(modelIndex.row(), modelIndex.column()) if isinstance(cell, DataEditCell): if editor.modified: newText = editor.contents() numLines = newText.count('\n') skipUndoAvail = numLines == self.prevNumLines self.prevNumLines = numLines treeStructure = globalref.mainControl.activeControl.structure undo.DataUndo(treeStructure.undoList, cell.node, False, False, skipUndoAvail, cell.field.name) try: cell.node.setData(cell.field, newText) except ValueError: editor.setErrorFlag() self.parent().nodeModified.emit(cell.node) cell.titleCellRef.setText(cell.node.title(cell.spot)) cell.typeCellRef.setText(cell.node.formatRef.name) editor.modified = False else: super().setModelData(editor, styleOption, modelIndex) def updateEditorGeometry(self, editor, styleOption, modelIndex): """Update the editor geometry to match the cell. Arguments: editor -- the editor widget styleOption -- the data for styles and geometry modelIndex -- the index of the cell to be painted """ editor.setMaximumSize(self.sizeHint(styleOption, modelIndex)) super().updateEditorGeometry(editor, styleOption, modelIndex) def adjustScroll(self): """Reset the scroll back to the original for unlimited height editors. Called from signal after any scroll change. Needed for non-hovering to fix late scroll reset after editor created. """ if (not globalref.genOptions['EditorLimitHeight'] and not globalref.genOptions['EditorOnHover'] and self.tallEditScrollPos >= 0): self.parent().verticalScrollBar().setValue(self.tallEditScrollPos) self.tallEditScrollPos = -1 def scrollOnKeyPress(self, editor): """Adjust the scroll position to make cursor visible. Needed after key presses on unlimited height editors. Arguments: editor -- the editor with the key press """ if not globalref.genOptions['EditorLimitHeight']: view = self.parent() cursorRect = editor.cursorRect() upperPos = editor.mapToGlobal(cursorRect.topLeft()).y() lowerPos = editor.mapToGlobal(cursorRect.bottomLeft()).y() viewRect = view.viewport().rect() viewTop = view.mapToGlobal(viewRect.topLeft()).y() viewBottom = view.mapToGlobal(viewRect.bottomLeft()).y() bar = view.verticalScrollBar() if upperPos < viewTop: bar.setValue(bar.value() - (viewTop - upperPos)) elif lowerPos > viewBottom: bar.setValue(bar.value() + (lowerPos - viewBottom)) def editorEvent(self, event, model, styleOption, modelIndex): """Save the mouse click position in order to set the editor's cursor. Arguments: event -- the mouse click event model -- the model (not used) styleOption -- the data for styles and geometry (not used) modelIndex -- the index of the cell (not used) """ if event.type() == QEvent.MouseButtonPress: self.editorClickPos = event.globalPos() # save scroll position for clicks on unlimited height editors self.tallEditScrollPos = self.parent().verticalScrollBar().value() return super().editorEvent(event, model, styleOption, modelIndex) def eventFilter(self, editor, event): """Override to handle various focus changes and control keys. Navigate away from this view if tab hit on end items. Catches tab before QDelegate's event filter on the editor. Also closes the editor if focus is lost for certain reasons. Arguments: editor -- the editor that Qt installed a filter on event -- the key press event """ if event.type() == QEvent.KeyPress: view = self.parent() if (event.key() == Qt.Key_Tab and view.currentRow() == view.rowCount() - 1): view.focusOtherView.emit(True) return True if (event.key() == Qt.Key_Backtab and view.currentRow() == 1): view.focusOtherView.emit(False) return True if (event.modifiers() == Qt.ControlModifier and Qt.Key_A <= event.key() <= Qt.Key_Z): key = QKeySequence(event.modifiers() | event.key()) view.shortcutEntered.emit(key) return True if event.type() == QEvent.MouseButtonPress: self.prevNumLines = -1 # reset undo avail for mouse cursor changes if event.type() == QEvent.FocusOut: self.prevNumLines = -1 # reset undo avail for any focus loss if (event.reason() in (Qt.MouseFocusReason, Qt.TabFocusReason, Qt.BacktabFocusReason) and (not hasattr(editor, 'calendar') or not editor.calendar or not editor.calendar.isVisible()) and (not hasattr(editor, 'intLinkDialog') or not editor.intLinkDialog or not editor.intLinkDialog.isVisible())): self.parent().endEditor() return True return super().eventFilter(editor, event) class DataEditView(QTableWidget): """Class override for the table-based data edit view. Sets view defaults and updates the content. """ nodeModified = pyqtSignal(treenode.TreeNode) inLinkSelectMode = pyqtSignal(bool) internalLinkSelected = pyqtSignal(treenode.TreeNode) focusOtherView = pyqtSignal(bool) hoverFocusActive = pyqtSignal() shortcutEntered = pyqtSignal(QKeySequence) def __init__(self, treeView, allActions, isChildView=True, parent=None): """Initialize the data edit view default view settings. Arguments: treeView - the tree view, needed for the current selection model allActions -- a dict containing actions for the editor context menu isChildView -- shows selected nodes if false, child nodes if true parent -- the parent main window """ super().__init__(0, 2, parent) self.treeView = treeView self.allActions = allActions self.isChildView = isChildView self.hideChildView = not globalref.genOptions['InitShowChildPane'] self.prevHoverCell = None self.inLinkSelectActive = False self.setAcceptDrops(True) self.setMouseTracking(globalref.genOptions['EditorOnHover']) self.horizontalHeader().hide() self.verticalHeader().hide() self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.verticalScrollBar().setSingleStep(self.fontMetrics(). lineSpacing()) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setItemDelegate(DataEditDelegate(self)) self.setEditTriggers(QAbstractItemView.NoEditTriggers) self.setShowGrid(False) pal = self.palette() pal.setBrush(QPalette.Base, QApplication.palette().window()) pal.setBrush(QPalette.Text, QApplication.palette().windowText()) self.setPalette(pal) self.currentItemChanged.connect(self.moveEditor) self.verticalScrollBar().valueChanged.connect(self.itemDelegate(). adjustScroll) def updateContents(self): """Reload the view's content if the view is shown. Avoids update if view is not visible or has zero height or width. """ selSpots = self.treeView.selectionModel().selectedSpots() if self.isChildView: if (len(selSpots) > 1 or self.hideChildView or (selSpots and not selSpots[0].nodeRef.childList)): self.hide() return if not selSpots: # use top node childList from tree structure selSpots = [globalref.mainControl.activeControl.structure. structSpot()] elif not selSpots: self.hide() return self.show() if not self.isVisible() or self.height() == 0 or self.width() == 0: return if self.isChildView: selSpots = selSpots[0].childSpots() self.clear() if selSpots: self.hide() # 2nd update very slow if shown during update self.setRowCount(100000) rowNum = -2 for spot in selSpots: rowNum = self.addNodeData(spot, rowNum + 2) self.setRowCount(rowNum + 1) self.adjustSizes() self.scrollToTop() self.show() def addNodeData(self, spot, startRow): """Populate the view with the data from the given node. Returns the last row number used. Arguments: spot -- the spot to add startRow -- the row offset """ node = spot.nodeRef formatName = node.formatRef.name typeCell = self.createInactiveCell(formatName) self.setItem(startRow, 0, typeCell) titleCell = self.createInactiveCell(node.title(spot)) self.setItem(startRow, 1, titleCell) fields = node.formatRef.fields() if not globalref.genOptions['EditNumbering']: fields = [field for field in fields if field.typeName != 'Numbering'] if not globalref.genOptions['ShowMath']: fields = [field for field in fields if field.typeName != 'Math'] row = 0 # initialize for cases with only Numbering or Math fields for row, field in enumerate(fields, startRow + 1): self.setItem(row, 0, self.createInactiveCell(field.name, Qt.AlignRight | Qt.AlignVCenter)) self.setItem(row, 1, DataEditCell(spot, field, titleCell, typeCell)) self.setItem(row + 1, 0, self.createInactiveCell('')) self.setItem(row + 1, 1, self.createInactiveCell('')) return row def updateUnselectedCells(self): """Refresh the data in active cells, keeping the cell structure. """ if not self.isVisible() or self.height() == 0 or self.width() == 0: return selSpots = self.treeView.selectionModel().selectedSpots() if self.isChildView: if not selSpots: # use top node childList from tree structure selSpots = [globalref.mainControl.activeControl.structure. structSpot()] selSpots = selSpots[0].childSpots() elif not selSpots: return rowNum = -2 for spot in selSpots: rowNum = self.refreshNodeData(spot, rowNum + 2) def refreshNodeData(self, spot, startRow): """Refresh the data in active cells for this node. Returns the last row number used. Arguments: node -- the node to add startRow -- the row offset """ node = spot.nodeRef self.item(startRow, 1).setText(node.title(spot)) fields = node.formatRef.fields() if not globalref.genOptions['EditNumbering']: fields = [field for field in fields if field.typeName != 'Numbering'] if not globalref.genOptions['ShowMath']: fields = [field for field in fields if field.typeName != 'Math'] for row, field in enumerate(fields, startRow + 1): cell = self.item(row, 1) if not cell.isSelected(): cell.updateText() return row @staticmethod def createInactiveCell(text, alignment=None): """Return a new inactive data edit view cell. Arguments: text -- the initial text string for the cell alignment -- the text alignment QT constant (None for default) """ cell = QTableWidgetItem(text) cell.setFlags(Qt.NoItemFlags) if alignment: cell.setTextAlignment(alignment) return cell def moveEditor(self, newCell, prevCell): """Close old editor and open new one based on new current cell. Arguments: newCell -- the new current edit cell item prevCell - the old current cell item """ try: if prevCell and hasattr(prevCell, 'updateText'): self.closePersistentEditor(prevCell) prevCell.updateText() self.resizeRowToContents(prevCell.row()) except RuntimeError: pass # avoid non-repeatable error involving deleted c++ object if newCell: self.openPersistentEditor(newCell) def endEditor(self): """End persistent editors by changing active cells. """ self.setCurrentCell(-1, -1) def setFont(self, font): """Override to avoid setting fonts of inactive cells. Arguments: font -- the font to set """ global defaultFont defaultFont = font def changeInLinkSelectMode(self, active=True): """Change the internal link select mode. Changes the internal variable (controlling hover) and signals the tree. Arguments: active -- if True, starts the mode, o/w ends """ self.inLinkSelectActive = active self.inLinkSelectMode.emit(active) def updateInLinkSelectMode(self, active=True): """Update the internal link select mode. Updates the internal variable (controlling hover). Arguments: active -- if True, starts the mode, o/w ends """ self.inLinkSelectActive = active def highlightSearch(self, wordList=None, regExpList=None): """Highlight any found search terms. Arguments: wordList -- list of words to highlight regExpList -- a list of regular expression objects to highlight """ backColor = self.palette().brush(QPalette.Active, QPalette.Highlight) foreColor = self.palette().brush(QPalette.Active, QPalette.HighlightedText) charFormat = QTextCharFormat() charFormat.setBackground(backColor) charFormat.setForeground(foreColor) spot = self.treeView.selectionModel().selectedSpots()[0] if wordList is None: wordList = [] if regExpList is None: regExpList = [] for regExp in regExpList: for match in regExp.finditer('\n'.join(spot.nodeRef. output(spotRef=spot))): matchText = match.group().lower() if matchText not in wordList: wordList.append(matchText) cells = [] completedCells = [] for word in wordList: cells.extend(self.findItems(word, Qt.MatchFixedString | Qt.MatchContains)) for cell in cells: if hasattr(cell, 'doc') and cell not in completedCells: highlighter = SearchHighlighter(wordList, charFormat, cell.doc) completedCells.append(cell) def highlightMatch(self, searchText='', regExpObj=None, cellNum=0, skipMatches=0): """Highlight a specific search result. Arguments: searchText -- the text to find in a non-regexp search regExpObj -- the regular expression to find if searchText is blank cellNum -- the vertical position (field number) of the cell skipMatches -- number of previous matches to skip in this field """ backColor = self.palette().brush(QPalette.Active, QPalette.Highlight) foreColor = self.palette().brush(QPalette.Active, QPalette.HighlightedText) charFormat = QTextCharFormat() charFormat.setBackground(backColor) charFormat.setForeground(foreColor) cellNum += 1 # skip title line cell = self.item(cellNum, 1) highlighter = MatchHighlighter(cell.doc, charFormat, searchText, regExpObj, skipMatches) def adjustSizes(self): """Update the column widths and row heights. """ self.resizeColumnToContents(0) if self.columnWidth(0) < _minColumnWidth: self.setColumnWidth(0, _minColumnWidth) self.setColumnWidth(1, max(self.width() - self.columnWidth(0) - self.verticalScrollBar().width() - 5, _minColumnWidth)) self.resizeRowsToContents() def focusInEvent(self, event): """Handle focus-in to start an editor when tab is used. Arguments: event -- the focus in event """ if event.reason() == Qt.TabFocusReason: for row in range(self.rowCount()): cell = self.item(row, 1) if hasattr(cell, 'doc'): self.setCurrentItem(cell) break elif event.reason() == Qt.BacktabFocusReason: for row in range(self.rowCount() - 1, -1, -1): cell = self.item(row, 1) if hasattr(cell, 'doc'): self.setCurrentItem(cell) break super().focusInEvent(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.updateContents() self.adjustSizes() return super().resizeEvent(event) def dragEnterEvent(self, event): """Accept drags of files to this window. Arguments: event -- the drag event object """ if event.mimeData().hasUrls(): event.accept() def dragMoveEvent(self, event): """Accept drags of files to this window. Arguments: event -- the drag event object """ cell = self.itemAt(event.pos()) if (isinstance(cell, DataEditCell) and getattr(dataeditors, cell.field.editorClassName).dragLinkEnabled): event.accept() else: event.ignore() def dropEvent(self, event): """Open a file dropped onto this window. Arguments: event -- the drop event object """ cell = self.itemAt(event.pos()) fileList = event.mimeData().urls() if fileList and isinstance(cell, DataEditCell): self.setCurrentItem(cell) self.setFocus() self.itemDelegate().lastEditor.addDroppedUrl(fileList[0]. toLocalFile()) def mousePressEvent(self, event): """Handle ctrl + click to follow links. Arguments: event -- the mouse event """ if (event.button() == Qt.LeftButton and event.modifiers() == Qt.ControlModifier): cell = self.itemAt(event.pos()) if cell and isinstance(cell, DataEditCell): xOffest = (event.pos().x() - self.columnViewportPosition(cell.column())) yOffset = (event.pos().y() - self.rowViewportPosition(cell.row())) pt = QPointF(xOffest, yOffset) pos = cell.doc.documentLayout().hitTest(pt, Qt.ExactHit) if pos >= 0: cursor = QTextCursor(cell.doc) cursor.setPosition(pos) address = cursor.charFormat().anchorHref() if address: if address.startswith('#'): (self.treeView.selectionModel(). selectNodeById(address[1:])) else: # check for relative path if urltools.isRelative(address): defaultPath = (globalref.mainControl. defaultPathObj(True)) address = urltools.toAbsolute(address, str(defaultPath)) dataeditors.openExtUrl(address) event.accept() else: super().mousePressEvent(event) def mouseMoveEvent(self, event): """Handle mouse move event to create editors on hover. Arguments: event -- the mouse event """ cell = self.itemAt(event.pos()) if cell and hasattr(cell, 'doc'): oldCell = self.currentItem() if (cell != oldCell and cell != self.prevHoverCell and not self.inLinkSelectActive): # save scroll position for unlimited height editors self.itemDelegate().tallEditScrollPos = (self. verticalScrollBar(). value()) self.prevHoverCell = cell self.hoverFocusActive.emit() self.setFocus() if oldCell and hasattr(oldCell, 'doc'): # these lines result in two calls to moveEditor, but seems # to be necessary to avoid race that leaves stray editors self.moveEditor(None, oldCell) self.setCurrentItem(None) try: self.setCurrentItem(cell) except RuntimeError: # catch error if view updates due to node rename ending pass else: self.prevHoverCell = None class SearchHighlighter(QSyntaxHighlighter): """Class override to highlight search terms in cell text. Used to highlight search words from a list. """ def __init__(self, wordList, charFormat, doc): """Initialize the highlighter with the text document. Arguments: wordList -- list of search terms charFormat -- the formatting to apply doc -- the text document """ super().__init__(doc) self.wordList = wordList self.charFormat = charFormat def highlightBlock(self, text): """Override method to highlight search terms in block of text. Arguments: text -- the text to highlight """ for word in self.wordList: pos = text.lower().find(word, 0) while pos >= 0: self.setFormat(pos, len(word), self.charFormat) pos = text.lower().find(word, pos + len(word)) class MatchHighlighter(QSyntaxHighlighter): """Class override to highlight a specific search result in cell text. Used to highlight a text or reg exp match. """ def __init__(self, doc, charFormat, searchText='', regExpObj=None, skipMatches=0): """Initialize the highlighter with the text document. Arguments: doc -- the text document charFormat -- the formatting to apply searchText -- the text to find if no regexp is given regExpObj -- the regular expression to find if given skipMatches -- number of previous matches to skip """ super().__init__(doc) self.charFormat = charFormat self.searchText = searchText self.regExpObj = regExpObj self.skipMatches = skipMatches def highlightBlock(self, text): """Override method to highlight a match in block of text. Arguments: text -- the text to highlight """ pos = matchLen = 0 for matchNum in range(self.skipMatches + 1): pos += matchLen if self.regExpObj: match = self.regExpObj.search(text, pos) pos = match.start() if match else -1 matchLen = len(match.group()) if match else 0 else: pos = text.lower().find(self.searchText, pos) matchLen = len(self.searchText) if pos >= 0: self.setFormat(pos, matchLen, self.charFormat) TreeLine/source/treeoutput.py0000644000175000017500000004227513363127527015360 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treeoutput.py, provides classes for output to views, html and printing # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import itertools from PyQt5.QtGui import QTextDocument import globalref _linkRe = re.compile(r']*href="#(.*?)"[^>]*>.*?', re.I | re.S) class OutputItem: """Class to store output for a single node. Stores text lines and original indent level. """ def __init__(self, spot, level): """Convert the spot's node into an output item. Create a blank item if spot is None. Arguments: spot -- the tree spot to convert level -- the node's original indent level """ if spot: node = spot.nodeRef nodeFormat = node.formatRef if not nodeFormat.useTables: self.textLines = [line + '
    ' for line in node.output(spotRef=spot)] else: self.textLines = node.output(keepBlanks=True, spotRef=spot) if not self.textLines: self.textLines = [''] self.addSpace = nodeFormat.spaceBetween self.siblingPrefix = nodeFormat.siblingPrefix self.siblingSuffix = nodeFormat.siblingSuffix if nodeFormat.useBullets and self.textLines: # remove
    extra space for bullets self.textLines[-1] = self.textLines[-1][:-6] self.uId = node.uId else: self.textLines = [''] self.addSpace = False self.siblingPrefix = '' self.siblingSuffix = '' self.uId = None self.level = level # following variables used by printdata only: self.height = 0 self.pageNum = 0 self.columnNum = 0 self.pagePos = 0 self.doc = None self.parentItem = None self.lastChildItem = None def duplicate(self): """Return an independent copy of this OutputItem. """ item = OutputItem(None, 0) item.textLines = self.textLines[:] item.addSpace = self.addSpace item.siblingPrefix = self.siblingPrefix item.siblingSuffix = self.siblingSuffix item.uId = self.uId item.level = self.level item.height = self.height item.pageNum = self.pageNum item.columnNum = self.columnNum item.pagePos = self.pagePos item.doc = None item.parentItem = self.parentItem item.lastChildItem = self.lastChildItem return item def addIndent(self, prevLevel, nextLevel): """Add
    tags to define indent levels in the output. Arguments: prevLevel -- the level of the previous item in the list nextLevel -- the level of the next item in the list """ for num in range(self.level - prevLevel): self.textLines[0] = '
    ' + self.textLines[0] for num in range(self.level - nextLevel): self.textLines[-1] += '
    ' def addAbsoluteIndent(self, pixels): """Add tags for an individual indentation. Removes the
    tag from the last line to avoid excess space, since
    starts a new line. The Qt output view does not fully support nested
    tags. Arguments: pixels -- the amount to indent """ self.textLines[0] = ('
    {1}'. format(pixels * self.level, self.textLines[0])) if not self.siblingPrefix and self.textLines[-1].endswith('
    '): self.textLines[-1] = self.textLines[-1][:-6] self.textLines[-1] += '
    ' def addSiblingPrefix(self): """Add the sibling prefix before this output. """ if self.siblingPrefix: self.textLines[0] = self.siblingPrefix + self.textLines[0] def addSiblingSuffix(self): """Add the sibling suffix after this output. """ if self.siblingSuffix: self.textLines[-1] += self.siblingSuffix def addAnchor(self): """Add a link anchor to this item. """ self.textLines[0] = '{1}'.format(self.uId, self.textLines[0]) def intLinkIds(self): """Return a set of uIDs from any internal links in this item. """ linkIds = set() for line in self.textLines: startPos = 0 while True: match = _linkRe.search(line, startPos) if not match: break uId = match.group(1) if uId: linkIds.add(uId) startPos = match.start(1) return linkIds def numLines(self): """Return the number of text lines in the item. """ return len(self.textLines) def equalPrefix(self, otherItem): """Return True if sibling prefixes and suffixes are equal. Arguments: otherItem -- the item to compare """ return (self.siblingPrefix == otherItem.siblingPrefix and self.siblingSuffix == otherItem.siblingSuffix) def setDocHeight(self, paintDevice, width, printFont, replaceDoc=False): """Set the height of this item for use in printer output. Creates an output document if not already created. Arguments: paintDevice -- the printer or other device for settings width -- the width available for the output text printFont -- the default font for the document replaceDoc -- if true, re-create the text document """ if not self.doc or replaceDoc: self.doc = QTextDocument() lines = '\n'.join(self.textLines) if lines.endswith('
    '): # remove trailing
    tag to avoid excess space lines = lines[:-6] self.doc.setHtml(lines) self.doc.setDefaultFont(printFont) frameFormat = self.doc.rootFrame().frameFormat() frameFormat.setBorder(0) frameFormat.setMargin(0) frameFormat.setPadding(0) self.doc.rootFrame().setFrameFormat(frameFormat) layout = self.doc.documentLayout() layout.setPaintDevice(paintDevice) self.doc.setTextWidth(width) self.height = layout.documentSize().height() def splitDocHeight(self, initHeight, maxHeight, paintDevice, width, printFont): """Split this item into two items and return them. The first item will fit into initHeight if practical. Splits at line endings if posible. Arguments: initHeight -- the preferred height of the first page maxheight -- the max height of any pages paintDevice -- the printer or other device for settings width -- the width available for the output text printFont -- the default font for the document """ newItem = self.duplicate() fullHeight = self.height lines = '\n'.join(self.textLines) allLines = [line + '
    ' for line in lines.split('
    ')] self.textLines = [] prevHeight = 0 for line in allLines: self.textLines.append(line) self.setDocHeight(paintDevice, width, printFont, True) if ((prevHeight and self.height > initHeight and fullHeight - prevHeight > maxHeight) or (prevHeight and self.height > maxHeight)): self.textLines = self.textLines[:-1] self.setDocHeight(paintDevice, width, printFont, True) newItem.textLines = allLines[len(self.textLines):] newItem.setDocHeight(paintDevice, width, printFont, True) return (self, newItem) if self.height > maxHeight: break prevHeight = self.height # no line ending breaks found text = ' \n'.join(allLines) allWords = [word + ' ' for word in text.split(' ')] newWords = [] prevHeight = 0 for word in allWords: if word.strip() == ' initHeight and fullHeight - prevHeight < maxHeight) or (prevHeight and self.height > maxHeight)): self.textLines = [''.join(newWords[:-1])] self.setDocHeight(paintDevice, width, printFont, True) newItem.textLines = [''.join(allWords[len(newWords):])] newItem.setDocHeight(paintDevice, width, printFont, True) return (self, newItem) if self.height > maxHeight: break prevHeight = self.height newItem.setDocHeight(paintDevice, width, printFont, True) return (newItem, None) # fail to split class OutputGroup(list): """A list of OutputItems that takes TreeNodes as input. Modifies the output text for use in views, html and printing. """ def __init__(self, spotList, includeRoot=True, includeDescend=False, openOnly=False): """Convert the node iter list into a list of output items. Arguments: spotList -- a list of spots to convert to output includeRoot -- if True, include the nodes in nodeList includeDescend -- if True, include children, grandchildren, etc. openOnly -- if true, ignore collapsed children in the main treeView """ super().__init__() for spot in spotList: level = -1 if includeRoot: level = 0 self.append(OutputItem(spot, level)) if includeDescend: self.addChildren(spot, level, openOnly) def addChildren(self, spot, level, openOnly=False): """Recursively add OutputItems for descendants of the given spot. Arguments: spot -- the parent tree spot level -- the parent node's original indent level """ treeView = globalref.mainControl.activeControl.activeWindow.treeView if not openOnly or treeView.isSpotExpanded(spot): for child in spot.childSpots(): self.append(OutputItem(child, level + 1)) self.addChildren(child, level + 1, openOnly) def addIndents(self): """Add nested
    elements to define indentations in the output. """ prevLevel = 0 for item, nextItem in itertools.zip_longest(self, self[1:]): try: nextLevel = nextItem.level except AttributeError: nextLevel = 0 item.addIndent(prevLevel, nextLevel) prevLevel = item.level def addAbsoluteIndents(self, pixels=20): """Add tags for individual indentation on each node. The Qt output view does not fully support nested
    tags. Arguments: pixels -- the amount to indent """ for item in self: item.addAbsoluteIndent(pixels) def addBlanksBetween(self): """Add blank lines between nodes based on node format's spaceBetween. """ for item, nextItem in zip(self, self[1:]): if item.addSpace or nextItem.addSpace: item.textLines[-1] += '
    ' def addAnchors(self, extraLevels=0): """Add anchors to items that are targets and to low level items. Arguments: extraLevels -- force adding anchors if level < this """ linkIds = set() for item in self: linkIds.update(item.intLinkIds()) for item in self: if item.uId in linkIds or item.level < extraLevels: item.addAnchor() def hasPrefixes(self): """Return True if sibling prefixes or suffixes are found. """ return bool([item for item in self if item.siblingPrefix or item.siblingSuffix]) def addSiblingPrefixes(self): """Add sibling prefixes and suffixes for each node. """ if not self.hasPrefixes(): return addPrefix = True for item, nextItem in itertools.zip_longest(self, self[1:]): if addPrefix: item.addSiblingPrefix() if (not nextItem or item.level != nextItem.level or not item.equalPrefix(nextItem)): item.addSiblingSuffix() addPrefix = True else: addPrefix = False def combineAllSiblings(self): """Group all sibling items with the same prefix into single items. Also add sibling prefixes and suffixes and spaces in between. """ newItems = [] prevItem = None for item in self: if prevItem: if item.level == prevItem.level and item.equalPrefix(prevItem): if item.addSpace or prevItem.addSpace: prevItem.textLines[-1] += '
    ' prevItem.textLines.extend(item.textLines) else: prevItem.addSiblingSuffix() newItems.append(prevItem) item.addSiblingPrefix() prevItem = item else: item.addSiblingPrefix() prevItem = item prevItem.addSiblingSuffix() newItems.append(prevItem) self[:] = newItems def combineLines(self, addSpacing=True, addPrefixes=True): """Return an OutputItem including all of the text from all items. Arguments: addPrefixes -- if True, add sibling prefix and suffix to result addSpacing -- if True, add spacing between items with addSpace True """ comboItem = self[0].duplicate() for item in self[1:]: if item.addSpace: comboItem.textLines[-1] += '
    ' comboItem.textLines.extend(item.textLines) if addPrefixes: comboItem.addSiblingPrefix() comboItem.addSiblingSuffix() return comboItem def splitColumns(self, numColumns): """Split output into even length columns using number of lines. Return a list with a group for each column. Arguments: numColumns - the number of columns to split """ if numColumns < 2: return [self] groupList = [] if len(self) <= numColumns: for item in self: groupList.append(OutputGroup([])) groupList[-1].append(item) return groupList numEach = len(self) // numColumns for colNum in range(numColumns - 1): groupList.append(OutputGroup([])) groupList[-1].extend(self[colNum * numEach : (colNum + 1) * numEach]) groupList.append(OutputGroup([])) groupList[-1].extend(self[(numColumns - 1) * numEach : ]) numChanges = 1 while numChanges: numChanges = 0 for colNum in range(numColumns - 1): if (groupList[colNum].totalNumLines() > groupList[colNum + 1]. totalNumLines() + groupList[colNum][-1].numLines()): groupList[colNum + 1].insert(0, groupList[colNum][-1]) del groupList[colNum][-1] numChanges += 1 if (groupList[colNum].totalNumLines() + groupList[colNum + 1][0].numLines() <= groupList[colNum + 1].totalNumLines()): groupList[colNum].append(groupList[colNum + 1][0]) del groupList[colNum + 1][0] numChanges += 1 return groupList def getLines(self): """Return the full list of text lines from this group. """ if not self: return [] lines = [] for item in self: lines.extend(item.textLines) return lines def totalNumLines(self): """Return the total number of lines of all items in this container. """ return sum([item.numLines() for item in self]) def loadFamilyRefs(self): """Set parentItem and lastChildItem for all items. Used by the printdata class. """ recentParents = [None] for item in self: if item.level > 0: item.parentItem = recentParents[item.level - 1] item.parentItem.lastChildItem = item try: recentParents[item.level] = item except IndexError: recentParents.append(item) TreeLine/source/treewindow.py0000644000175000017500000012047613671465220015324 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treewindow.py, provides a class for the main window and controls # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import pathlib import base64 from PyQt5.QtCore import QEvent, QRect, QSize, Qt, pyqtSignal from PyQt5.QtGui import QGuiApplication, QTextDocument from PyQt5.QtWidgets import (QAction, QActionGroup, QApplication, QMainWindow, QSplitter, QStackedWidget, QStatusBar, QTabWidget) import treeview import breadcrumbview import outputview import dataeditview import titlelistview import treenode import globalref class TreeWindow(QMainWindow): """Class override for the main window. Contains main window views and controls. """ selectChanged = pyqtSignal() nodeModified = pyqtSignal(treenode.TreeNode) treeModified = pyqtSignal() winActivated = pyqtSignal(QMainWindow) winMinimized = pyqtSignal() winClosing = pyqtSignal(QMainWindow) def __init__(self, model, allActions, parent=None): """Initialize the main window. Arguments: model -- the initial data model allActions -- a dict containing the upper level actions parent -- the parent window, usually None """ super().__init__(parent) self.allActions = allActions.copy() self.allowCloseFlag = True self.winActions = {} self.toolbars = [] self.rightTabActList = [] self.setAttribute(Qt.WA_DeleteOnClose) self.setAcceptDrops(True) self.setStatusBar(QStatusBar()) self.setCaption() self.setupActions() self.setupMenus() self.setupToolbars() self.restoreToolbarPosition() self.treeView = treeview.TreeView(model, self.allActions) self.breadcrumbSplitter = QSplitter(Qt.Vertical) self.setCentralWidget(self.breadcrumbSplitter) self.breadcrumbView = breadcrumbview.BreadcrumbView(self.treeView) self.breadcrumbSplitter.addWidget(self.breadcrumbView) self.breadcrumbView.setVisible(globalref. genOptions['InitShowBreadcrumb']) self.treeSplitter = QSplitter() self.breadcrumbSplitter.addWidget(self.treeSplitter) self.treeStack = QStackedWidget() self.treeSplitter.addWidget(self.treeStack) self.treeStack.addWidget(self.treeView) self.treeView.shortcutEntered.connect(self.execShortcut) self.treeView.selectionModel().selectionChanged.connect(self. updateRightViews) self.treeFilterView = None self.rightTabs = QTabWidget() self.treeSplitter.addWidget(self.rightTabs) self.rightTabs.setTabPosition(QTabWidget.South) self.rightTabs.tabBar().setFocusPolicy(Qt.NoFocus) self.outputSplitter = QSplitter(Qt.Vertical) self.rightTabs.addTab(self.outputSplitter, _('Data Output')) parentOutputView = outputview.OutputView(self.treeView, False) parentOutputView.highlighted[str].connect(self.statusBar().showMessage) self.outputSplitter.addWidget(parentOutputView) childOutputView = outputview.OutputView(self.treeView, True) childOutputView.highlighted[str].connect(self.statusBar().showMessage) self.outputSplitter.addWidget(childOutputView) self.editorSplitter = QSplitter(Qt.Vertical) self.rightTabs.addTab(self.editorSplitter, _('Data Edit')) parentEditView = dataeditview.DataEditView(self.treeView, self.allActions, False) parentEditView.shortcutEntered.connect(self.execShortcut) parentEditView.focusOtherView.connect(self.focusNextView) parentEditView.inLinkSelectMode.connect(self.treeView. toggleNoMouseSelectMode) self.treeView.skippedMouseSelect.connect(parentEditView. internalLinkSelected) self.editorSplitter.addWidget(parentEditView) childEditView = dataeditview.DataEditView(self.treeView, self.allActions, True) childEditView.shortcutEntered.connect(self.execShortcut) childEditView.focusOtherView.connect(self.focusNextView) childEditView.inLinkSelectMode.connect(self.treeView. toggleNoMouseSelectMode) self.treeView.skippedMouseSelect.connect(childEditView. internalLinkSelected) parentEditView.hoverFocusActive.connect(childEditView.endEditor) childEditView.hoverFocusActive.connect(parentEditView.endEditor) parentEditView.inLinkSelectMode.connect(childEditView. updateInLinkSelectMode) childEditView.inLinkSelectMode.connect(parentEditView. updateInLinkSelectMode) self.editorSplitter.addWidget(childEditView) self.titleSplitter = QSplitter(Qt.Vertical) self.rightTabs.addTab(self.titleSplitter, _('Title List')) parentTitleView = titlelistview.TitleListView(self.treeView, False) parentTitleView.shortcutEntered.connect(self.execShortcut) self.titleSplitter.addWidget(parentTitleView) childTitleView = titlelistview.TitleListView(self.treeView, True) childTitleView.shortcutEntered.connect(self.execShortcut) self.titleSplitter.addWidget(childTitleView) self.rightTabs.currentChanged.connect(self.updateRightViews) self.updateFonts() def setExternalSignals(self): """Connect widow object signals to signals in this object. In a separate method to refresh after local control change. """ self.treeView.selectionModel().selectionChanged.connect(self. selectChanged) for i in range(2): self.editorSplitter.widget(i).nodeModified.connect(self. nodeModified) self.titleSplitter.widget(i).nodeModified.connect(self. nodeModified) self.titleSplitter.widget(i).treeModified.connect(self. treeModified) def updateActions(self, allActions): """Use new actions for menus, etc. when the local control changes. Arguments: allActions -- a dict containing the upper level actions """ # remove submenu actions that are children of the window self.removeAction(self.allActions['DataNodeType']) self.removeAction(self.allActions['FormatFontSize']) self.allActions = allActions.copy() self.allActions.update(self.winActions) self.menuBar().clear() self.setupMenus() self.addToolbarCommands() self.treeView.allActions = self.allActions for i in range(2): self.editorSplitter.widget(i).allActions = self.allActions def updateTreeNode(self, node): """Update all spots for the given node in the tree view. Arguments: node -- the node to be updated """ for spot in node.spotRefs: self.treeView.update(spot.index(self.treeView.model())) self.treeView.resizeColumnToContents(0) self.breadcrumbView.updateContents() def updateTree(self): """Update the full tree view. """ self.treeView.scheduleDelayedItemsLayout() self.breadcrumbView.updateContents() def updateRightViews(self, *args, outputOnly=False): """Update all right-hand views and breadcrumb view. Arguments: *args -- dummy arguments to collect args from signals outputOnly -- only update output views (not edit views) """ if globalref.mainControl.activeControl: self.rightTabActList[self.rightTabs. currentIndex()].setChecked(True) self.breadcrumbView.updateContents() splitter = self.rightTabs.currentWidget() if not outputOnly or isinstance(splitter.widget(0), outputview.OutputView): for i in range(2): splitter.widget(i).updateContents() def refreshDataEditViews(self): """Refresh the data in non-selected cells in curreent data edit views. """ splitter = self.rightTabs.currentWidget() if isinstance(splitter.widget(0), dataeditview.DataEditView): for i in range(2): splitter.widget(i).updateUnselectedCells() def updateCommandsAvail(self): """Set window commands available based on node selections. """ self.allActions['ViewPrevSelect'].setEnabled(len(self.treeView. selectionModel(). prevSpots) > 1) self.allActions['ViewNextSelect'].setEnabled(len(self.treeView. selectionModel(). nextSpots) > 0) def updateWinGenOptions(self): """Update tree and data edit windows based on general option changes. """ self.treeView.updateTreeGenOptions() for i in range(2): self.editorSplitter.widget(i).setMouseTracking(globalref. genOptions['EditorOnHover']) def updateFonts(self): """Update custom fonts in views. """ treeFont = QTextDocument().defaultFont() treeFontName = globalref.miscOptions['TreeFont'] if treeFontName: treeFont.fromString(treeFontName) self.treeView.setFont(treeFont) self.treeView.updateTreeGenOptions() if self.treeFilterView: self.treeFilterView.setFont(treeFont) ouputFont = QTextDocument().defaultFont() ouputFontName = globalref.miscOptions['OutputFont'] if ouputFontName: ouputFont.fromString(ouputFontName) editorFont = QTextDocument().defaultFont() editorFontName = globalref.miscOptions['EditorFont'] if editorFontName: editorFont.fromString(editorFontName) for i in range(2): self.outputSplitter.widget(i).setFont(ouputFont) self.editorSplitter.widget(i).setFont(editorFont) self.titleSplitter.widget(i).setFont(editorFont) def resetTreeModel(self, model): """Change the model assigned to the tree view. Arguments: model -- the new model to assign """ self.treeView.resetModel(model) self.treeView.selectionModel().selectionChanged.connect(self. updateRightViews) def activateAndRaise(self): """Activate this window and raise it to the front. """ self.activateWindow() self.raise_() def setCaption(self, pathObj=None, modified=False): """Change the window caption title based on the file name and path. Arguments: pathObj - a path object for the current file """ modFlag = '*' if modified else '' if pathObj: caption = '{0}{1} [{2}] - TreeLine'.format(str(pathObj.name), modFlag, str(pathObj.parent)) else: caption = '- TreeLine' self.setWindowTitle(caption) def filterView(self): """Create, show and return a filter view. """ self.removeFilterView() self.treeFilterView = treeview.TreeFilterView(self.treeView, self.allActions) self.treeFilterView.shortcutEntered.connect(self.execShortcut) self.treeView.selectionModel().selectionChanged.connect(self. treeFilterView. updateFromSelectionModel) for i in range(2): editView = self.editorSplitter.widget(i) editView.inLinkSelectMode.connect(self.treeFilterView. toggleNoMouseSelectMode) self.treeFilterView.skippedMouseSelect.connect(editView. internalLinkSelected) self.treeStack.addWidget(self.treeFilterView) self.treeStack.setCurrentWidget(self.treeFilterView) return self.treeFilterView def removeFilterView(self): """Hide and delete the current filter view. """ if self.treeFilterView != None: # check for None since False if empty self.treeStack.removeWidget(self.treeFilterView) globalref.mainControl.currentStatusBar().removeWidget(self. treeFilterView. messageLabel) self.treeFilterView.messageLabel.deleteLater() self.treeFilterView = None def rightParentView(self): """Return the current right-hand parent view if visible (or None). """ view = self.rightTabs.currentWidget().widget(0) if not view.isVisible() or view.height() == 0 or view.width() == 0: return None return view def rightChildView(self): """Return the current right-hand parent view if visible (or None). """ view = self.rightTabs.currentWidget().widget(1) if not view.isVisible() or view.height() == 0 or view.width() == 0: return None return view def focusNextView(self, forward=True): """Focus the next pane in the tab focus series. Called by a signal from the data edit views. Tab sequences tend to skip views without this. Arguments: forward -- forward in tab series if True """ reason = (Qt.TabFocusReason if forward else Qt.BacktabFocusReason) rightParent = self.rightParentView() rightChild = self.rightChildView() if (self.sender().isChildView == forward or (forward and rightChild == None) or (not forward and rightParent == None)): self.treeView.setFocus(reason) elif forward: rightChild.setFocus(reason) else: rightParent.setFocus(reason) def execShortcut(self, key): """Execute an action based on a shortcut key signal from a view. Arguments: key -- the QKeySequence shortcut """ keyDict = {action.shortcut().toString(): action for action in self.allActions.values()} try: action = keyDict[key.toString()] except KeyError: return if action.isEnabled(): action.trigger() def setupActions(self): """Add the actions for contols at the window level. These actions only affect an individual window, they're independent in multiple windows of the same file. """ viewExpandBranchAct = QAction(_('&Expand Full Branch'), self, statusTip=_('Expand all children of the selected nodes')) viewExpandBranchAct.triggered.connect(self.viewExpandBranch) self.winActions['ViewExpandBranch'] = viewExpandBranchAct viewCollapseBranchAct = QAction(_('&Collapse Full Branch'), self, statusTip=_('Collapse all children of the selected nodes')) viewCollapseBranchAct.triggered.connect(self.viewCollapseBranch) self.winActions['ViewCollapseBranch'] = viewCollapseBranchAct viewPrevSelectAct = QAction(_('&Previous Selection'), self, statusTip=_('Return to the previous tree selection')) viewPrevSelectAct.triggered.connect(self.viewPrevSelect) self.winActions['ViewPrevSelect'] = viewPrevSelectAct viewNextSelectAct = QAction(_('&Next Selection'), self, statusTip=_('Go to the next tree selection in history')) viewNextSelectAct.triggered.connect(self.viewNextSelect) self.winActions['ViewNextSelect'] = viewNextSelectAct viewRightTabGrp = QActionGroup(self) viewOutputAct = QAction(_('Show Data &Output'), viewRightTabGrp, statusTip=_('Show data output in right view'), checkable=True) self.winActions['ViewDataOutput'] = viewOutputAct viewEditAct = QAction(_('Show Data &Editor'), viewRightTabGrp, statusTip=_('Show data editor in right view'), checkable=True) self.winActions['ViewDataEditor'] = viewEditAct viewTitleAct = QAction(_('Show &Title List'), viewRightTabGrp, statusTip=_('Show title list in right view'), checkable=True) self.winActions['ViewTitleList'] = viewTitleAct self.rightTabActList = [viewOutputAct, viewEditAct, viewTitleAct] viewRightTabGrp.triggered.connect(self.viewRightTab) viewBreadcrumbAct = QAction(_('Show &Breadcrumb View'), self, statusTip=_('Toggle showing breadcrumb ancestor view'), checkable=True) viewBreadcrumbAct.setChecked(globalref. genOptions['InitShowBreadcrumb']) viewBreadcrumbAct.triggered.connect(self.viewBreadcrumb) self.winActions['ViewBreadcrumb'] = viewBreadcrumbAct viewChildPaneAct = QAction(_('&Show Child Pane'), self, statusTip=_('Toggle showing right-hand child views'), checkable=True) viewChildPaneAct.setChecked(globalref.genOptions['InitShowChildPane']) viewChildPaneAct.triggered.connect(self.viewShowChildPane) self.winActions['ViewShowChildPane'] = viewChildPaneAct viewDescendAct = QAction(_('Show Output &Descendants'), self, statusTip=_('Toggle showing output view indented descendants'), checkable=True) viewDescendAct.setChecked(globalref.genOptions['InitShowDescendants']) viewDescendAct.triggered.connect(self.viewDescendants) self.winActions['ViewShowDescend'] = viewDescendAct winCloseAct = QAction(_('&Close Window'), self, statusTip=_('Close this window')) winCloseAct.triggered.connect(self.close) self.winActions['WinCloseWindow'] = winCloseAct incremSearchStartAct = QAction(_('Start Incremental Search'), self) incremSearchStartAct.triggered.connect(self.incremSearchStart) self.addAction(incremSearchStartAct) self.winActions['IncremSearchStart'] = incremSearchStartAct incremSearchNextAct = QAction(_('Next Incremental Search'), self) incremSearchNextAct.triggered.connect(self.incremSearchNext) self.addAction(incremSearchNextAct) self.winActions['IncremSearchNext'] = incremSearchNextAct incremSearchPrevAct = QAction(_('Previous Incremental Search'), self) incremSearchPrevAct.triggered.connect(self.incremSearchPrev) self.addAction(incremSearchPrevAct) self.winActions['IncremSearchPrev'] = incremSearchPrevAct for name, action in self.winActions.items(): icon = globalref.toolIcons.getIcon(name.lower()) if icon: action.setIcon(icon) key = globalref.keyboardOptions[name] if not key.isEmpty(): action.setShortcut(key) self.allActions.update(self.winActions) def setupToolbars(self): """Add toolbars based on option settings. """ for toolbar in self.toolbars: self.removeToolBar(toolbar) self.toolbars = [] numToolbars = globalref.toolbarOptions['ToolbarQuantity'] iconSize = globalref.toolbarOptions['ToolbarSize'] for num in range(numToolbars): name = 'Toolbar{:d}'.format(num) toolbar = self.addToolBar(name) toolbar.setObjectName(name) toolbar.setIconSize(QSize(iconSize, iconSize)) self.toolbars.append(toolbar) self.addToolbarCommands() def addToolbarCommands(self): """Add toolbar commands for current actions. """ for toolbar, commandList in zip(self.toolbars, globalref. toolbarOptions['ToolbarCommands']): toolbar.clear() for command in commandList.split(','): if command: try: toolbar.addAction(self.allActions[command]) except KeyError: pass else: toolbar.addSeparator() def setupMenus(self): """Add menu items for actions. """ self.fileMenu = self.menuBar().addMenu(_('&File')) self.fileMenu.aboutToShow.connect(self.loadRecentMenu) self.fileMenu.addAction(self.allActions['FileNew']) self.fileMenu.addAction(self.allActions['FileOpen']) self.fileMenu.addAction(self.allActions['FileOpenSample']) self.fileMenu.addAction(self.allActions['FileImport']) self.fileMenu.addSeparator() self.fileMenu.addAction(self.allActions['FileSave']) self.fileMenu.addAction(self.allActions['FileSaveAs']) self.fileMenu.addAction(self.allActions['FileExport']) self.fileMenu.addAction(self.allActions['FileProperties']) self.fileMenu.addSeparator() self.fileMenu.addAction(self.allActions['FilePrintSetup']) self.fileMenu.addAction(self.allActions['FilePrintPreview']) self.fileMenu.addAction(self.allActions['FilePrint']) self.fileMenu.addAction(self.allActions['FilePrintPdf']) self.fileMenu.addSeparator() self.recentFileSep = self.fileMenu.addSeparator() self.fileMenu.addAction(self.allActions['FileQuit']) editMenu = self.menuBar().addMenu(_('&Edit')) editMenu.addAction(self.allActions['EditUndo']) editMenu.addAction(self.allActions['EditRedo']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditCut']) editMenu.addAction(self.allActions['EditCopy']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditPaste']) editMenu.addAction(self.allActions['EditPastePlain']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditPasteChild']) editMenu.addAction(self.allActions['EditPasteBefore']) editMenu.addAction(self.allActions['EditPasteAfter']) editMenu.addSeparator() editMenu.addAction(self.allActions['EditPasteCloneChild']) editMenu.addAction(self.allActions['EditPasteCloneBefore']) editMenu.addAction(self.allActions['EditPasteCloneAfter']) nodeMenu = self.menuBar().addMenu(_('&Node')) nodeMenu.addAction(self.allActions['NodeRename']) nodeMenu.addSeparator() nodeMenu.addAction(self.allActions['NodeAddChild']) nodeMenu.addAction(self.allActions['NodeInsertBefore']) nodeMenu.addAction(self.allActions['NodeInsertAfter']) nodeMenu.addSeparator() nodeMenu.addAction(self.allActions['NodeDelete']) nodeMenu.addAction(self.allActions['NodeIndent']) nodeMenu.addAction(self.allActions['NodeUnindent']) nodeMenu.addSeparator() nodeMenu.addAction(self.allActions['NodeMoveUp']) nodeMenu.addAction(self.allActions['NodeMoveDown']) nodeMenu.addAction(self.allActions['NodeMoveFirst']) nodeMenu.addAction(self.allActions['NodeMoveLast']) dataMenu = self.menuBar().addMenu(_('&Data')) # add action's parent to get the sub-menu dataMenu.addMenu(self.allActions['DataNodeType'].parent()) # add the action to activate the shortcut key self.addAction(self.allActions['DataNodeType']) dataMenu.addAction(self.allActions['DataConfigType']) dataMenu.addAction(self.allActions['DataCopyType']) dataMenu.addAction(self.allActions['DataVisualConfig']) dataMenu.addSeparator() dataMenu.addAction(self.allActions['DataSortNodes']) dataMenu.addAction(self.allActions['DataNumbering']) dataMenu.addAction(self.allActions['DataRegenRefs']) dataMenu.addSeparator() dataMenu.addAction(self.allActions['DataCloneMatches']) dataMenu.addAction(self.allActions['DataDetachClones']) dataMenu.addSeparator() dataMenu.addAction(self.allActions['DataFlatCategory']) dataMenu.addAction(self.allActions['DataAddCategory']) dataMenu.addAction(self.allActions['DataSwapCategory']) toolsMenu = self.menuBar().addMenu(_('&Tools')) toolsMenu.addAction(self.allActions['ToolsFindText']) toolsMenu.addAction(self.allActions['ToolsFindCondition']) toolsMenu.addAction(self.allActions['ToolsFindReplace']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsFilterText']) toolsMenu.addAction(self.allActions['ToolsFilterCondition']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsSpellCheck']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsGenOptions']) toolsMenu.addSeparator() toolsMenu.addAction(self.allActions['ToolsShortcuts']) toolsMenu.addAction(self.allActions['ToolsToolbars']) toolsMenu.addAction(self.allActions['ToolsFonts']) toolsMenu.addAction(self.allActions['ToolsColors']) formatMenu = self.menuBar().addMenu(_('Fo&rmat')) formatMenu.addAction(self.allActions['FormatBoldFont']) formatMenu.addAction(self.allActions['FormatItalicFont']) formatMenu.addAction(self.allActions['FormatUnderlineFont']) formatMenu.addSeparator() # add action's parent to get the sub-menu formatMenu.addMenu(self.allActions['FormatFontSize'].parent()) # add the action to activate the shortcut key self.addAction(self.allActions['FormatFontSize']) formatMenu.addAction(self.allActions['FormatFontColor']) formatMenu.addSeparator() formatMenu.addAction(self.allActions['FormatExtLink']) formatMenu.addAction(self.allActions['FormatIntLink']) formatMenu.addAction(self.allActions['FormatInsertDate']) formatMenu.addSeparator() formatMenu.addAction(self.allActions['FormatSelectAll']) formatMenu.addAction(self.allActions['FormatClearFormat']) viewMenu = self.menuBar().addMenu(_('&View')) viewMenu.addAction(self.allActions['ViewExpandBranch']) viewMenu.addAction(self.allActions['ViewCollapseBranch']) viewMenu.addSeparator() viewMenu.addAction(self.allActions['ViewPrevSelect']) viewMenu.addAction(self.allActions['ViewNextSelect']) viewMenu.addSeparator() viewMenu.addAction(self.allActions['ViewDataOutput']) viewMenu.addAction(self.allActions['ViewDataEditor']) viewMenu.addAction(self.allActions['ViewTitleList']) viewMenu.addSeparator() viewMenu.addAction(self.allActions['ViewBreadcrumb']) viewMenu.addAction(self.allActions['ViewShowChildPane']) viewMenu.addAction(self.allActions['ViewShowDescend']) self.windowMenu = self.menuBar().addMenu(_('&Window')) self.windowMenu.aboutToShow.connect(self.loadWindowMenu) self.windowMenu.addAction(self.allActions['WinNewWindow']) self.windowMenu.addAction(self.allActions['WinCloseWindow']) self.windowMenu.addSeparator() helpMenu = self.menuBar().addMenu(_('&Help')) helpMenu.addAction(self.allActions['HelpBasic']) helpMenu.addAction(self.allActions['HelpFull']) helpMenu.addSeparator() helpMenu.addAction(self.allActions['HelpAbout']) def viewExpandBranch(self): """Expand all children of the selected spots. """ QApplication.setOverrideCursor(Qt.WaitCursor) selectedSpots = self.treeView.selectionModel().selectedSpots() if not selectedSpots: selectedSpots = self.treeView.model().treeStructure.rootSpots() for spot in selectedSpots: self.treeView.expandBranch(spot) QApplication.restoreOverrideCursor() def viewCollapseBranch(self): """Collapse all children of the selected spots. """ QApplication.setOverrideCursor(Qt.WaitCursor) selectedSpots = self.treeView.selectionModel().selectedSpots() if not selectedSpots: selectedSpots = self.treeView.model().treeStructure.rootSpots() for spot in selectedSpots: self.treeView.collapseBranch(spot) QApplication.restoreOverrideCursor() def viewPrevSelect(self): """Return to the previous tree selection. """ self.treeView.selectionModel().restorePrevSelect() def viewNextSelect(self): """Go to the next tree selection in history. """ self.treeView.selectionModel().restoreNextSelect() def viewRightTab(self, action): """Show the tab in the right-hand view given by action. Arguments: action -- the action triggered in the action group """ if action == self.allActions['ViewDataOutput']: self.rightTabs.setCurrentWidget(self.outputSplitter) elif action == self.allActions['ViewDataEditor']: self.rightTabs.setCurrentWidget(self.editorSplitter) else: self.rightTabs.setCurrentWidget(self.titleSplitter) def viewBreadcrumb(self, checked): """Enable or disable the display of the breadcrumb view. Arguments: checked -- True if to be shown, False if to be hidden """ self.breadcrumbView.setVisible(checked) if checked: self.updateRightViews() def viewShowChildPane(self, checked): """Enable or disable the display of children in a split pane. Arguments: checked -- True if to be shown, False if to be hidden """ for tabNum in range(3): for splitNum in range(2): view = self.rightTabs.widget(tabNum).widget(splitNum) view.hideChildView = not checked self.updateRightViews() def viewDescendants(self, checked): """Set the output view to show indented descendants if checked. Arguments: checked -- True if to be shown, False if to be hidden """ self.outputSplitter.widget(1).showDescendants = checked self.updateRightViews() def incremSearchStart(self): """Start an incremental title search. """ if not self.treeFilterView: self.treeView.setFocus() self.treeView.incremSearchStart() def incremSearchNext(self): """Go to the next match in an incremental title search. """ if not self.treeFilterView: self.treeView.incremSearchNext() def incremSearchPrev(self): """Go to the previous match in an incremental title search. """ if not self.treeFilterView: self.treeView.incremSearchPrev() def loadRecentMenu(self): """Load recent file items to file menu before showing. """ for action in self.fileMenu.actions(): text = action.text() if len(text) > 1 and text[0] == '&' and '0' <= text[1] <= '9': self.fileMenu.removeAction(action) self.fileMenu.insertActions(self.recentFileSep, globalref.mainControl.recentFiles. getActions()) def loadWindowMenu(self): """Load window list items to window menu before showing. """ for action in self.windowMenu.actions(): text = action.text() if len(text) > 1 and text[0] == '&' and '0' <= text[1] <= '9': self.windowMenu.removeAction(action) self.windowMenu.addActions(globalref.mainControl.windowActions()) def saveWindowGeom(self): """Save window geometry parameters to history options. """ contentsRect = self.geometry() frameRect = self.frameGeometry() globalref.histOptions.changeValue('WindowXSize', contentsRect.width()) globalref.histOptions.changeValue('WindowYSize', contentsRect.height()) globalref.histOptions.changeValue('WindowXPos', contentsRect.x()) globalref.histOptions.changeValue('WindowYPos', contentsRect.y()) globalref.histOptions.changeValue('WindowTopMargin', contentsRect.y() - frameRect.y()) globalref.histOptions.changeValue('WindowOtherMargin', contentsRect.x() - frameRect.x()) try: upperWidth, lowerWidth = self.breadcrumbSplitter.sizes() crumbPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('CrumbSplitPercent', crumbPercent) leftWidth, rightWidth = self.treeSplitter.sizes() treePercent = int(100 * leftWidth / (leftWidth + rightWidth)) globalref.histOptions.changeValue('TreeSplitPercent', treePercent) upperWidth, lowerWidth = self.outputSplitter.sizes() outputPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('OutputSplitPercent', outputPercent) upperWidth, lowerWidth = self.editorSplitter.sizes() editorPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('EditorSplitPercent', editorPercent) upperWidth, lowerWidth = self.titleSplitter.sizes() titlePercent = int(100 * upperWidth / (upperWidth + lowerWidth)) globalref.histOptions.changeValue('TitleSplitPercent', titlePercent) except ZeroDivisionError: pass # skip if splitter sizes were never set tabNum = self.rightTabs.currentIndex() globalref.histOptions.changeValue('ActiveRightView', tabNum) def restoreWindowGeom(self, offset=0): """Restore window geometry from history options. Arguments: offset -- number of pixels to offset window, down and to right """ rect = QRect(globalref.histOptions['WindowXPos'], globalref.histOptions['WindowYPos'], globalref.histOptions['WindowXSize'], globalref.histOptions['WindowYSize']) if rect.x() == -1000 and rect.y() == -1000: # let OS position window the first time self.resize(rect.size()) else: if offset: rect.adjust(offset, offset, offset, offset) availRect = QApplication.primaryScreen().availableVirtualGeometry() topMargin = globalref.histOptions['WindowTopMargin'] otherMargin = globalref.histOptions['WindowOtherMargin'] # remove frame space from available rect availRect.adjust(otherMargin, topMargin, -otherMargin, -otherMargin) finalRect = rect.intersected(availRect) if finalRect.isEmpty(): rect.moveTo(0, 0) finalRect = rect.intersected(availRect) if finalRect.isValid(): self.setGeometry(finalRect) crumbWidth = int(self.breadcrumbSplitter.width() / 100 * globalref.histOptions['CrumbSplitPercent']) self.breadcrumbSplitter.setSizes([crumbWidth, self.breadcrumbSplitter.width() - crumbWidth]) treeWidth = int(self.treeSplitter.width() / 100 * globalref.histOptions['TreeSplitPercent']) self.treeSplitter.setSizes([treeWidth, self.treeSplitter.width() - treeWidth]) outHeight = int(self.outputSplitter.height() / 100.0 * globalref.histOptions['OutputSplitPercent']) self.outputSplitter.setSizes([outHeight, self.outputSplitter.height() - outHeight]) editHeight = int(self.editorSplitter.height() / 100.0 * globalref.histOptions['EditorSplitPercent']) self.editorSplitter.setSizes([editHeight, self.editorSplitter.height() - editHeight]) titleHeight = int(self.titleSplitter.height() / 100.0 * globalref.histOptions['TitleSplitPercent']) self.titleSplitter.setSizes([titleHeight, self.titleSplitter.height() - titleHeight]) self.rightTabs.setCurrentIndex(globalref. histOptions['ActiveRightView']) def resetWindowGeom(self): """Set all stored window geometry values back to default settings. """ globalref.histOptions.resetToDefaults(['WindowXPos', 'WindowYPos', 'WindowXSize', 'WindowYSize', 'CrumbSplitPercent', 'TreeSplitPercent', 'OutputSplitPercent', 'EditorSplitPercent', 'TitleSplitPercent', 'ActiveRightView']) def saveToolbarPosition(self): """Save the toolbar position to the toolbar options. """ toolbarPos = base64.b64encode(self.saveState().data()).decode('ascii') globalref.toolbarOptions.changeValue('ToolbarPosition', toolbarPos) globalref.toolbarOptions.writeFile() def restoreToolbarPosition(self): """Restore the toolbar position from the toolbar options. """ toolbarPos = globalref.toolbarOptions['ToolbarPosition'] if toolbarPos: self.restoreState(base64.b64decode(bytes(toolbarPos, 'ascii'))) def dragEnterEvent(self, event): """Accept drags of files to this window. Arguments: event -- the drag event object """ if event.mimeData().hasUrls(): event.accept() def dropEvent(self, event): """Open a file dropped onto this window. Arguments: event -- the drop event object """ fileList = event.mimeData().urls() if fileList: path = pathlib.Path(fileList[0].toLocalFile()) globalref.mainControl.openFile(path, checkModified=True) def changeEvent(self, event): """Detect an activation of the main window and emit a signal. Arguments: event -- the change event object """ super().changeEvent(event) if (event.type() == QEvent.ActivationChange and QApplication.activeWindow() == self): self.winActivated.emit(self) elif (event.type() == QEvent.WindowStateChange and globalref.genOptions['MinToSysTray'] and self.isMinimized()): self.winMinimized.emit() def closeEvent(self, event): """Signal that the view is closing and close if the flag allows it. Also save window status if necessary. Arguments: event -- the close event object """ self.winClosing.emit(self) if self.allowCloseFlag: event.accept() else: event.ignore() TreeLine/source/colorset.py0000644000175000017500000002564113635454705014773 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # colorset.py, provides storage/retrieval and dialogs for GUI colors # # TreeLine, an information storage program # Copyright (C) 2020, 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 enum from collections import OrderedDict from PyQt5.QtCore import pyqtSignal, Qt, QEvent, QObject from PyQt5.QtGui import QColor, QFontMetrics, QPalette, QPixmap from PyQt5.QtWidgets import (QApplication, QColorDialog, QComboBox, QDialog, QFrame, QGroupBox, QHBoxLayout, QLabel, QGridLayout, QPushButton, QVBoxLayout, qApp) import globalref roles = OrderedDict([('Window', _('Dialog background color')), ('WindowText', _('Dialog text color')), ('Base', _('Text widget background color')), ('Text', _('Text widget foreground color')), ('Highlight', _('Selected item background color')), ('HighlightedText', _('Selected item text color')), ('Link', _('Link text color')), ('ToolTipBase', _('Tool tip background color')), ('ToolTipText', _('Tool tip foreground color')), ('Button', _('Button background color')), ('ButtonText', _('Button text color')), ('Text-Disabled', _('Disabled text foreground color')), ('ButtonText-Disabled', _('Disabled button text color'))]) ThemeSetting = enum.IntEnum('ThemeSetting', 'system dark custom') darkColors = {'Window': '#353535', 'WindowText': '#ffffff', 'Base': '#191919', 'Text': '#ffffff', 'Highlight': '#2a82da', 'HighlightedText': '#000000', 'Link': '#2a82da', 'ToolTipBase': '#000080', 'ToolTipText': '#c0c0c0', 'Button': '#353535', 'ButtonText': '#ffffff', 'Text-Disabled': '#808080', 'ButtonText-Disabled': '#808080'} class ColorSet: """Stores color settings and provides dialogs for user changes. """ def __init__(self): """Initialize colors settings from the system or from options. """ self.sysPalette = QApplication.palette() self.colors = [Color(roleKey) for roleKey in roles.keys()] self.theme = ThemeSetting[globalref.miscOptions['ColorTheme']] for color in self.colors: color.colorChanged.connect(self.setCustomTheme) color.setFromPalette(self.sysPalette) if self.theme == ThemeSetting.dark: color.setFromTheme(darkColors) elif self.theme == ThemeSetting.custom: color.setFromOption() def setAppColors(self): """Set application to current colors. """ newPalette = QApplication.palette() for color in self.colors: color.updatePalette(newPalette) qApp.setPalette(newPalette) def showDialog(self, parent): """Show a dialog for user color changes. Return True if changes were made. Arguments: parent -- the parent widget for the dialog """ dialog = QDialog(parent) dialog.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowSystemMenuHint) dialog.setWindowTitle(_('Color Settings')) topLayout = QVBoxLayout(dialog) dialog.setLayout(topLayout) themeBox = QGroupBox(_('Color Theme'), dialog) topLayout.addWidget(themeBox) themeLayout = QVBoxLayout(themeBox) self.themeControl = QComboBox(dialog) self.themeControl.addItem(_('Default system theme'), ThemeSetting.system) self.themeControl.addItem(_('Dark theme'), ThemeSetting.dark) self.themeControl.addItem(_('Custom theme'), ThemeSetting.custom) self.themeControl.setCurrentIndex(self.themeControl. findData(self.theme)) self.themeControl.currentIndexChanged.connect(self.updateThemeSetting) themeLayout.addWidget(self.themeControl) self.groupBox = QGroupBox(dialog) self.setBoxTitle() topLayout.addWidget(self.groupBox) gridLayout = QGridLayout(self.groupBox) row = 0 for color in self.colors: gridLayout.addWidget(color.getLabel(), row, 0) gridLayout.addWidget(color.getSwatch(), row, 1) row += 1 ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QPushButton(_('&OK'), dialog) ctrlLayout.addWidget(okButton) okButton.clicked.connect(dialog.accept) cancelButton = QPushButton(_('&Cancel'), dialog) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(dialog.reject) if dialog.exec_() == QDialog.Accepted: self.theme = ThemeSetting(self.themeControl.currentData()) globalref.miscOptions.changeValue('ColorTheme', self.theme.name) if self.theme == ThemeSetting.system: qApp.setPalette(self.sysPalette) else: # dark theme or custom if self.theme == ThemeSetting.custom: for color in self.colors: color.updateOption() self.setAppColors() globalref.miscOptions.writeFile() else: for color in self.colors: color.setFromPalette(self.sysPalette) if self.theme == ThemeSetting.dark: color.setFromTheme(darkColors) elif self.theme == ThemeSetting.custom: color.setFromOption() def setBoxTitle(self): """Set title of group box to standard or custom. """ if self.themeControl.currentData() == ThemeSetting.custom: title = _('Custom Colors') else: title = _('Theme Colors') self.groupBox.setTitle(title) def updateThemeSetting(self): """Update the colors based on a theme control change. """ if self.themeControl.currentData() == ThemeSetting.system: for color in self.colors: color.setFromPalette(self.sysPalette) color.changeSwatchColor() elif self.themeControl.currentData() == ThemeSetting.dark: for color in self.colors: color.setFromTheme(darkColors) color.changeSwatchColor() else: for color in self.colors: color.setFromOption() color.changeSwatchColor() self.setBoxTitle() def setCustomTheme(self): """Set to custom theme setting after user color change. """ if self.themeControl.currentData != ThemeSetting.custom: self.themeControl.blockSignals(True) self.themeControl.setCurrentIndex(2) self.themeControl.blockSignals(False) self.setBoxTitle() class Color(QObject): """Stores a single color setting for a role. """ colorChanged = pyqtSignal() def __init__(self, roleKey, parent=None): """Initialize a Color. Arguments: roleKey -- the text name of the color role parent -- a parent object if given """ super().__init__(parent) self.roleKey = roleKey if '-' in roleKey: roleStr, groupStr = roleKey.split('-') self.group = eval('QPalette.' + groupStr) else: roleStr = roleKey self.group = None self.role = eval('QPalette.' + roleStr) self.currentColor = None self.swatch = None def setFromPalette(self, palette): """Set the color based on the given palette. Arguments: palette -- the palette that defines the color """ if self.group: self.currentColor = palette.color(self.group, self.role) else: self.currentColor = palette.color(self.role) def setFromOption(self): """Set color based on the option setting. """ colorStr = globalref.miscOptions[self.roleKey + 'Color'] color = QColor(colorStr) if color.isValid(): self.currentColor = color def setFromTheme(self, theme): """Set color based on the given theme dictionary. Arguments: theme -- a theme dictionary that defines the color """ self.currentColor = QColor(theme[self.roleKey]) def updateOption(self): """Set the option to the current color. """ if self.currentColor: globalref.miscOptions.changeValue(self.roleKey + 'Color', self.currentColor.name()) def updatePalette(self, palette): """Set the role in the given palette to the current color. Arguments: palette -- the palette that gets set with the color """ if self.group: palette.setColor(self.group, self.role, self.currentColor) else: palette.setColor(self.role, self.currentColor) def getLabel(self): """Return a label for this role in a dialog. """ return QLabel(roles[self.roleKey]) def getSwatch(self): """Return a label color swatch with the current color. """ self.swatch = QLabel() self.changeSwatchColor() self.swatch.setFrameStyle(QFrame.Panel | QFrame.Raised) self.swatch.setLineWidth(3) self.swatch.installEventFilter(self) return self.swatch def changeSwatchColor(self): """Set swatch to currentColor. """ height = QFontMetrics(self.swatch.font()).height() pixmap = QPixmap(3 * height, height) pixmap.fill(self.currentColor) self.swatch.setPixmap(pixmap) def eventFilter(self, obj, event): """Handle mouse clicks on swatches. Arguments: obj -- the object to handle events for event -- the specific event """ if obj == self.swatch and event.type() == QEvent.MouseButtonRelease: color = QColorDialog.getColor(self.currentColor, QApplication.activeWindow(), _('Select {0} color'). format(self.roleKey)) if color.isValid() and color != self.currentColor: self.currentColor = color self.changeSwatchColor() self.colorChanged.emit() return True return False TreeLine/source/imports.py0000644000175000017500000012502013534772201014616 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # imports.py, provides classes for a file import dialog and import functions # # TreeLine, an information storage program # Copyright (C) 2019, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import pathlib import re import collections import zipfile import csv import html.parser import xml.sax.saxutils from xml.etree import ElementTree from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QMessageBox import miscdialogs import treenode import treestructure import treemodel import nodeformat import treeformats import urltools import globalref methods = collections.OrderedDict() methods.update([(_('Text'), None), (_('&Tab indented text, one node per line'), 'importTabbedText'), (_('Co&mma delimited (CSV) text table with level column && ' 'header row'), 'importTableCsvLevels'), (_('Comma delimited (CSV) text table &with header row'), 'importTableCsv'), (_('Tab delimited text table with header &row'), 'importTableTabbed'), (_('Plain text, one &node per line (CR delimited)'), 'importTextLines'), (_('Plain text ¶graphs (blank line delimited)'), 'importTextPara'), (_('Bookmarks'), None), (_('&HTML bookmarks (Mozilla Format)'), 'importMozilla'), (_('&XML bookmarks (XBEL format)'), 'importXbel'), (_('Other'), None), (_('Old Tree&Line File (1.x or 2.x)'), 'importOldTreeLine'), (_('Treepad &file (text nodes only)'), 'importTreePad'), (_('&Generic XML (non-TreeLine file)'), 'importXml'), (_('Open &Document (ODF) outline'), 'importOdfText')]) fileFilters = {'importTabbedText': 'txt', 'importTableCsvLevels': 'csv', 'importTableCsv': 'csv', 'importTableTabbed': 'txt', 'importTextLines': 'txt', 'importTextPara': 'txt', 'importMozilla': 'html', 'importXbel': 'xml', 'importOldTreeLine': 'trl', 'importTreePad': 'hjt', 'importXml': 'xml', 'importOdfText': 'odt'} oldDateTimeConv = {'d': '%-d', 'dd': '%d', 'ddd': '%a', 'dddd': '%A', 'M': '%-m', 'MM': '%m', 'MMM': '%b', 'MMMM': '%B', 'yy': '%y', 'yyyy': '%Y', 'H': '%-H', 'HH': '%H', 'h': '%-I', 'hh': '%I', 'm': '%-M', 'mm': '%M', 's': '%-S', 'ss': '%S', 'zzz': '%f', 'AP': '%p', 'ap': '%p'} bookmarkFolderTypeName = _('FOLDER') bookmarkLinkTypeName = _('BOOKMARK') bookmarkSeparatorTypeName = _('SEPARATOR') bookmarkLinkFieldName = _('Link') textFieldName = _('Text') genericXmlTextFieldName = 'Element_Data' htmlUnescapeDict = {'amp': '&', 'lt': '<', 'gt': '>', 'quot': '"'} class ImportControl: """Control file imports of alt file types. """ def __init__(self, pathObj=None): """Initialize the import control object. Arguments: pathObj -- the path object to import if given, o/w prompt user """ self.pathObj = pathObj self.errorMessage = '' # below members for old TreeLine file imports self.treeLineImportVersion = [] self.treeLineRootAttrib = {} self.treeLineOldFieldAttr = {} def interactiveImport(self, addWarning=False): """Prompt the user for import type & proceed with import. Return the structure if import is successful, otherwise None Arguments: addWarning - if True, add non-valid file warning to dialog """ dialog = miscdialogs.RadioChoiceDialog(_('Import File'), _('Choose Import Method'), methods.items(), QApplication. activeWindow()) if addWarning: fileName = self.pathObj.name dialog.addLabelBox(_('Invalid File'), _('"{0}" is not a valid TreeLine file.\n\n' 'Use an import filter?').format(fileName)) if dialog.exec_() != QDialog.Accepted: return None method = dialog.selectedButton() if not self.pathObj: filters = ';;'.join((globalref.fileFilters[fileFilters[method]], globalref.fileFilters['all'])) defaultFilePath = str(globalref.mainControl.defaultPathObj(True)) filePath, selFltr = QFileDialog.getOpenFileName(QApplication. activeWindow(), _('TreeLine - Import File'), defaultFilePath, filters) if not filePath: return None self.pathObj = pathlib.Path(filePath) self.errorMessage = '' try: QApplication.setOverrideCursor(Qt.WaitCursor) structure = getattr(self, method)() QApplication.restoreOverrideCursor() except IOError: QApplication.restoreOverrideCursor() QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - could not read file {0}'). format(self.pathObj)) return None if not structure: message = _('Error - improper format in {0}').format(self.pathObj) if self.errorMessage: message = '{0}\n{1}'.format(message, self.errorMessage) self.errorMessage = '' QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', message) return structure def importTabbedText(self): """Import a file with tabbed title structure. Return the structure if import is successful, otherwise None """ structure = treestructure.TreeStructure(addDefaults=True, addSpots=False) formatRef = structure.childList[0].formatRef structure.removeNodeDictRef(structure.childList[0]) structure.childList = [] nodeList = [] with self.pathObj.open(encoding=globalref.localTextEncoding) as f: for line in f: text = line.strip() if text: level = line.count('\t', 0, len(line) - len(line.lstrip())) node = treenode.TreeNode(formatRef) node.setTitle(text) structure.addNodeDictRef(node) nodeList.append((node, level)) if nodeList and structure.loadChildNodeLevels(nodeList): structure.generateSpots(None) return structure return None def importTableCsvLevels(self): """Import a CSV-delimited table file with level column, header row. Return the structure if import is successful, otherwise None. """ structure = treestructure.TreeStructure(addSpots=False) tableFormat = nodeformat.NodeFormat(_('TABLE'), structure.treeFormats) structure.treeFormats.addTypeIfMissing(tableFormat) nodeList = [] with self.pathObj.open(newline='', encoding=globalref.localTextEncoding) as f: reader = csv.reader(f) try: headings = [self.correctFieldName(name) for name in next(reader)][1:] tableFormat.addFieldList(headings, True, True) for entries in reader: if entries: node = treenode.TreeNode(tableFormat) structure.addNodeDictRef(node) try: level = int(entries.pop(0)) except ValueError: self.errorMessage = (_('Invalid level number on ' 'line {0}'). format(reader.line_num)) return None # abort nodeList.append((node, level)) try: for heading in headings: node.data[heading] = entries.pop(0) except IndexError: pass # fewer entries than headings is OK if entries: self.errorMessage = (_('Too many entries on ' 'Line {0}'). format(reader.line_num)) return None # abort if too few headings except csv.Error: self.errorMessage = (_('Bad CSV format on Line {0}'). format(reader.line_num)) return None # abort if nodeList: if structure.loadChildNodeLevels(nodeList): structure.generateSpots(None) return structure self.errorMessage = (_('Invalid level structure')) return None def importTableCsv(self): """Import a file with a CSV-delimited table with header row. Return the structure if import is successful, otherwise None. """ structure = treestructure.TreeStructure(addDefaults=True, addSpots=False) tableFormat = nodeformat.NodeFormat(_('TABLE'), structure.treeFormats) structure.treeFormats.addTypeIfMissing(tableFormat) with self.pathObj.open(newline='', encoding=globalref.localTextEncoding) as f: reader = csv.reader(f) try: headings = [self.correctFieldName(name) for name in next(reader)] tableFormat.addFieldList(headings, True, True) for entries in reader: if entries: node = treenode.TreeNode(tableFormat) structure.childList[0].childList.append(node) structure.addNodeDictRef(node) try: for heading in headings: node.data[heading] = entries.pop(0) except IndexError: pass # fewer entries than headings is OK if entries: self.errorMessage = (_('Too many entries on ' 'Line {0}'). format(reader.line_num)) return None # abort if too few headings except csv.Error: self.errorMessage = (_('Bad CSV format on Line {0}'). format(reader.line_num)) return None # abort structure.generateSpots(None) return structure def importTableTabbed(self): """Import a file with a tab-delimited table with header row. Return the structure if import is successful, otherwise None. """ structure = treestructure.TreeStructure(addDefaults=True, addSpots=False) tableFormat = nodeformat.NodeFormat(_('TABLE'), structure.treeFormats) structure.treeFormats.addTypeIfMissing(tableFormat) with self.pathObj.open(encoding=globalref.localTextEncoding) as f: headings = [self.correctFieldName(name) for name in f.readline().split('\t')] tableFormat.addFieldList(headings, True, True) lineNum = 1 for line in f: lineNum += 1 if line.strip(): entries = line.split('\t') node = treenode.TreeNode(tableFormat) structure.childList[0].childList.append(node) structure.addNodeDictRef(node) try: for heading in headings: node.data[heading] = entries.pop(0) except IndexError: pass # fewer entries than headings is OK if entries: self.errorMessage = (_('Too many entries on Line {0}'). format(lineNum)) return None # abort if too few headings structure.generateSpots(None) return structure @staticmethod def correctFieldName(name): """Return the field name with any illegal characters removed. Arguments: name -- the name to modify """ name = re.sub(r'[^\w_\-.]', '_', name.strip()) if not name: return 'X' if not name[0].isalpha() or name[:3].lower() == 'xml': name = 'X' + name return name def importTextLines(self): """Import a text file, creating one node per line. Return the structure if import is successful, otherwise None. """ structure = treestructure.TreeStructure(addDefaults=True, addSpots=False) nodeFormat = structure.childList[0].formatRef structure.removeNodeDictRef(structure.childList[0]) structure.childList = [] with self.pathObj.open(encoding=globalref.localTextEncoding) as f: for line in f: line = line.strip() if line: node = treenode.TreeNode(nodeFormat) structure.childList.append(node) structure.addNodeDictRef(node) node.data[nodeformat.defaultFieldName] = line structure.generateSpots(None) return structure def importTextPara(self): """Import a text file, creating one node per paragraph. Blank line delimited. Return the structure if import is successful, otherwise None. """ structure = treestructure.TreeStructure(addDefaults=True, addSpots=False) nodeFormat = structure.childList[0].formatRef structure.removeNodeDictRef(structure.childList[0]) structure.childList = [] with self.pathObj.open(encoding=globalref.localTextEncoding) as f: text = f.read() paraList = text.split('\n\n') for para in paraList: para = para.strip() if para: node = treenode.TreeNode(nodeFormat) structure.childList.append(node) structure.addNodeDictRef(node) node.data[nodeformat.defaultFieldName] = para structure.generateSpots(None) return structure def importOldTreeLine(self): """Import an old TreeLine File (1.x or 2.x). Return the structure if import is successful, otherwise None. """ tree = ElementTree.ElementTree() try: tree.parse(str(self.pathObj)) except ElementTree.ParseError: tree = None if not tree or not tree.getroot().get('item') == 'y': fileObj = self.pathObj.open('rb') # decompress before decrypt to support TreeLine 1.4 and earlier fileObj, compressed = globalref.mainControl.decompressFile(fileObj) fileObj, encrypted = globalref.mainControl.decryptFile(fileObj) if not fileObj: return None if encrypted and not compressed: fileObj, compressed = (globalref.mainControl. decompressFile(fileObj)) if compressed or encrypted: tree = ElementTree.ElementTree() try: tree.parse(fileObj) except ElementTree.ParseError: tree = None fileObj.close() if not tree or not tree.getroot().get('item') == 'y': return None version = tree.getroot().get('tlversion', '').split('.') try: self.treeLineImportVersion = [int(i) for i in version] except ValueError: pass self.treeLineRootAttrib = self.convertPrintData(tree.getroot().attrib) structure = treestructure.TreeStructure() idRefDict = {} linkList = [] self.loadOldTreeLineNode(tree.getroot(), structure, idRefDict, linkList, None) self.convertOldNodes(structure) linkRe = re.compile(r'
    ]*href="#(.*?)"[^>]*>.*?', re.I | re.S) for node, fieldName in linkList: text = node.data[fieldName] startPos = 0 while True: match = linkRe.search(text, startPos) if not match: break newId = idRefDict.get(match.group(1), '') if newId: text = text[:match.start(1)] + newId + text[match.end(1):] startPos = match.start(1) node.data[fieldName] = text structure.generateSpots(None) if nodeformat.FileInfoFormat.typeName in structure.treeFormats: fileFormat = structure.treeFormats[nodeformat.FileInfoFormat. typeName] structure.treeFormats.fileInfoFormat.duplicateFileInfo(fileFormat) del structure.treeFormats[nodeformat.FileInfoFormat.typeName] structure.treeFormats.updateDerivedRefs() for nodeFormat in structure.treeFormats.values(): nodeFormat.updateLineParsing() return structure def loadOldTreeLineNode(self, element, structure, idRefDict, linkList, parent=None): """Recursively load an old TreeLine ElementTree node and its children. Arguments: element -- an ElementTree node structure -- a ref to the new tree structure idRefDict -- a dict to relate old to new unique node IDs linkList -- internal link list ref with (node, fieldname) tuples parent -- the parent TreeNode (None for the root node only) """ try: typeFormat = structure.treeFormats[element.tag] except KeyError: formatData = self.convertOldNodeFormat(element.attrib) typeFormat = nodeformat.NodeFormat(element.tag, structure.treeFormats, formatData) structure.treeFormats[element.tag] = typeFormat self.treeLineOldFieldAttr[typeFormat.name] = {} if element.get('item') == 'y': node = treenode.TreeNode(typeFormat) oldId = element.attrib.get('uniqueid', '') if oldId: idRefDict[oldId] = node.uId if parent: parent.childList.append(node) else: structure.childList.append(node) structure.nodeDict[node.uId] = node cloneAttr = element.attrib.get('clones', '') if cloneAttr: for cloneId in cloneAttr.split(','): if cloneId in idRefDict: cloneNode = structure.nodeDict[idRefDict[cloneId]] node.data = cloneNode.data.copy() break else: # bare format (no nodes) node = None for child in element: if child.get('item') and node: self.loadOldTreeLineNode(child, structure, idRefDict, linkList, node) else: if node and child.text: node.data[child.tag] = child.text if child.get('linkcount'): linkList.append((node, child.tag)) if child.tag not in typeFormat.fieldDict: fieldData = self.convertOldFieldFormat(child.attrib) oldFormatDict = self.treeLineOldFieldAttr[typeFormat.name] oldFormatDict[child.tag] = fieldData typeFormat.addField(child.tag, fieldData) def convertPrintData(self, attrib): """Return JSON print data from old root attributes. Arguments: attrib -- old root print data attributes """ for key in ('printlines', 'printwidowcontrol', 'printportrait'): if key in attrib: attrib[key] = not attrib[key].startswith('n') for key in ('printindentfactor', 'printpaperwidth', 'printpaperheight', 'printheadermargin', 'printfootermargin', 'printcolumnspace'): if key in attrib: attrib[key] = float(attrib[key]) if 'printmargins' in attrib: attrib['printmargins'] = [float(margin) for margin in attrib['printmargins'].split()] if 'printnumcolumns' in attrib: attrib['printnumcolumns'] = int(attrib['printnumcolumns']) return attrib def convertOldNodeFormat(self, attrib): """Return JSON format data from old node format attributes. Arguments: attrib -- old node format attrib dict """ for key in ('spacebetween', 'formathtml', 'bullets', 'tables'): if key in attrib: attrib[key] = attrib[key].startswith('y') attrib['titleline'] = attrib.get('line0', '') lineKeyRe = re.compile(r'line\d+$') lineNums = sorted([int(key[4:]) for key in attrib.keys() if lineKeyRe.match(key)]) if lineNums and lineNums[0] == 0: del lineNums[0] attrib['outputlines'] = [attrib['line{0}'.format(keyNum)] for keyNum in lineNums] if self.treeLineImportVersion < [1, 9]: # for very old TL versions attrib['spacebetween'] = not (self.treeLineRootAttrib. get('nospace', '').startswith('y')) attrib['formathtml'] = not (self.treeLineRootAttrib. get('nohtml', '').startswith('y')) return attrib def convertOldFieldFormat(self, attrib): """Return JSON format data from old field format attributes. Arguments: attrib -- old field node format attrib dict """ fieldType = attrib.get('type', '') if fieldType: attrib['fieldtype'] = fieldType fieldFormat = attrib.get('format', '') if self.treeLineImportVersion < [1, 9]: # for very old TL versions if fieldType in ('URL', 'Path', 'ExecuteLink', 'Email'): attrib['oldfieldtype'] = fieldType fieldType = 'ExternalLink' attrib['fieldtype'] = fieldType if fieldType == 'Date': fieldFormat = fieldFormat.replace('w', 'd') fieldFormat = fieldFormat.replace('m', 'M') if fieldType == 'Time': fieldFormat = fieldFormat.replace('M', 'm') fieldFormat = fieldFormat.replace('s', 'z') fieldFormat = fieldFormat.replace('S', 's') fieldFormat = fieldFormat.replace('AA', 'AP') fieldFormat = fieldFormat.replace('aa', 'ap') if 'lines' in attrib: attrib['lines'] = int(attrib['lines']) if 'sortkeynum' in attrib: attrib['sortkeynum'] = int(attrib['sortkeynum']) if 'sortkeydir' in attrib: attrib['sortkeyfwd'] = not attrib['sortkeydir'].startswith('r') if 'evalhtml' in attrib: attrib['evalhtml'] = attrib['evalhtml'].startswith('y') if fieldType in ('Date', 'Time', 'DateTime'): origFormat = fieldFormat fieldFormat = '' while origFormat: replLen = 4 while replLen > 0: if origFormat[:replLen] in oldDateTimeConv: fieldFormat += oldDateTimeConv[origFormat[:replLen]] origFormat = origFormat[replLen:] break replLen -= 1 if replLen == 0: fieldFormat += origFormat[0] origFormat = origFormat[1:] if fieldFormat: attrib['format'] = fieldFormat return attrib def convertOldNodes(self, structure): """Convert node data to new date and time formats. Arguments: structure -- the ref structure containing the data """ for node in structure.nodeDict.values(): for field in node.formatRef.fields(): text = node.data.get(field.name, '') if text: if field.typeName in ('Date', 'DateTime'): text = text.replace('/', '-') if field.typeName in ('Time', 'DateTime'): text = text + '.000000' if self.treeLineImportVersion < [1, 9]: # very old TL ver oldFormatDict = self.treeLineOldFieldAttr[node. formatRef.name] oldFieldAttr = oldFormatDict[field.name] if (field.typeName == 'Text' and not oldFieldAttr.get('html', '').startswith('y')): text = text.strip() text = xml.sax.saxutils.escape(text) text = text.replace('\n', '
    ') elif (field.typeName == 'ExternalLink' and oldFieldAttr.get('oldfieldtype', '')): oldType = oldFieldAttr['oldfieldtype'] linkAltField = oldFieldAttr.get('linkalt', '') dispName = node.data.get(linkAltField, '') if not dispName: dispName = text if oldType == 'URL': if not urltools.extractScheme(text): text = urltools.replaceScheme('http', text) elif oldType == 'Path': text = urltools.replaceScheme('file', text) elif oldType == 'ExecuteLink': if urltools.isRelative(text): fullPath = urltools.which(text) if fullPath: text = fullPath text = urltools.replaceScheme('file', text) elif oldType == 'Email': text = urltools.replaceScheme('mailto', text) text = '{1}'.format(text, dispName) elif field.typeName == 'InternalLink': linkAltField = oldFieldAttr.get('linkalt', '') dispName = node.data.get(linkAltField, '') if not dispName: dispName = text uniqueId = text.strip().split('\n', 1)[0] uniqueId = uniqueId.replace(' ', '_').lower() uniqueId = re.sub(r'[^a-zA-Z0-9_-]+', '', uniqueId) text = '{1}'.format(uniqueId, dispName) elif field.typeName == 'Picture': text = ''.format(text) node.data[field.name] = text def importTreePad(self): """Import a Treepad file, text nodes only. Return the model if import is successful, otherwise None. """ structure = treestructure.TreeStructure(addDefaults=True, addSpots=False) structure.removeNodeDictRef(structure.childList[0]) structure.childList = [] tpFormat = structure.treeFormats[treeformats.defaultTypeName] tpFormat.addFieldList([textFieldName], False, True) tpFormat.fieldDict[textFieldName].changeType('SpacedText') try: with self.pathObj.open(encoding=globalref.localTextEncoding) as f: textList = f.read().split(' 5P9i0s8y19Z') except UnicodeDecodeError: with self.pathObj.open(encoding='latin-1') as f: textList = f.read().split(' 5P9i0s8y19Z') except UnicodeDecodeError: return None nodeList = [] 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): return None node = treenode.TreeNode(tpFormat) node.data[nodeformat.defaultFieldName] = title node.data[textFieldName] = '\n'.join(lines) node.level = level nodeList.append(node) structure.addNodeDictRef(node) parentList = [] for node in nodeList: if node.level != 0: parentList = parentList[:node.level] node.parent = parentList[-1] parentList[-1].childList.append(node) parentList.append(node) structure.childList = [nodeList[0]] structure.generateSpots(None) return structure def importXml(self): """Import a non-treeline generic XML file. Return the structure if import is successful, otherwise None. """ structure = treestructure.TreeStructure() tree = ElementTree.ElementTree() try: tree.parse(str(self.pathObj)) self.loadXmlNode(tree.getroot(), structure, None) except ElementTree.ParseError: return None for elemFormat in structure.treeFormats.values(): if not elemFormat.getTitleLine(): # fix formats if required elemFormat.changeTitleLine(elemFormat.name) for fieldName in elemFormat.fieldNames(): elemFormat.addOutputLine('{0}="{{*{1}*}}"'. format(fieldName, fieldName)) if not elemFormat.fieldDict: elemFormat.addField(genericXmlTextFieldName) if structure.childList: structure.generateSpots(None) return structure return None def loadXmlNode(self, element, structure, parent=None): """Recursively load a generic XML ElementTree node and its children. Arguments: element -- an XML ElementTree node structure -- a ref to the TreeLine structure parent -- the parent TreeNode (None for the root node only) """ elemFormat = structure.treeFormats.get(element.tag, None) if not elemFormat: elemFormat = nodeformat.NodeFormat(element.tag, structure.treeFormats) structure.treeFormats[element.tag] = elemFormat node = treenode.TreeNode(elemFormat) structure.addNodeDictRef(node) if not parent: parent = structure parent.childList.append(node) if element.text and element.text.strip(): if genericXmlTextFieldName not in elemFormat.fieldDict: elemFormat.addFieldList([genericXmlTextFieldName], True, True) node.setTitle(element.text.strip()) for key, value in element.items(): elemFormat.addFieldIfNew(key) node.data[key] = value for child in element: self.loadXmlNode(child, structure, node) def importOdfText(self): """Import an ODF format text file outline. Return the structure if import is successful, otherwise None. """ structure = treestructure.TreeStructure(addDefaults=True, addSpots=False) structure.removeNodeDictRef(structure.childList[0]) structure.childList = [] odfFormat = structure.treeFormats[treeformats.defaultTypeName] odfFormat.addField(textFieldName) odfFormat.changeOutputLines(['{{*{0}*}}'. format(nodeformat.defaultFieldName), '{{*{0}*}}'.format(textFieldName)]) odfFormat.formatHtml = True try: with zipfile.ZipFile(str(self.pathObj), 'r') as f: text = f.read('content.xml') except (zipfile.BadZipFile, KeyError): return None try: rootElement = ElementTree.fromstring(text) except ElementTree.ParseError: return None nameSpace = '{urn:oasis:names:tc:opendocument:xmlns:text:1.0}' headerTag = '{0}h'.format(nameSpace) paraTag = '{0}p'.format(nameSpace) numRegExp = re.compile(r'.*?(\d+)$') parents = [structure] prevLevel = 0 for elem in rootElement.iter(): if elem.tag == headerTag: style = elem.get('{0}style-name'.format(nameSpace), '') try: level = int(numRegExp.match(style).group(1)) except AttributeError: return None if level < 1 or level > prevLevel + 1: return None parents = parents[:level] node = treenode.TreeNode(odfFormat) structure.addNodeDictRef(node) parents[-1].childList.append(node) node.data[nodeformat.defaultFieldName] = ''.join(elem. itertext()) parents.append(node) prevLevel = level elif elem.tag == paraTag: text = ''.join(elem.itertext()) origText = node.data.get(textFieldName, '') if origText: text = '{0}
    {1}'.format(origText, text) node.data[textFieldName] = text structure.generateSpots(None) return structure def createBookmarkFormat(self): """Return a set of node formats for bookmark imports. """ treeFormats = treeformats.TreeFormats() folderFormat = nodeformat.NodeFormat(bookmarkFolderTypeName, treeFormats, addDefaultField=True) folderFormat.iconName = 'folder_3' treeFormats[folderFormat.name] = folderFormat linkFormat = nodeformat.NodeFormat(bookmarkLinkTypeName, treeFormats, addDefaultField=True) linkFormat.addField(bookmarkLinkFieldName, {'fieldtype': 'ExternalLink'}) linkFormat.addOutputLine('{{*{0}*}}'.format(bookmarkLinkFieldName)) linkFormat.iconName = 'bookmark' treeFormats[linkFormat.name] = linkFormat sepFormat = nodeformat.NodeFormat(bookmarkSeparatorTypeName, treeFormats, {'formathtml': True}, True) sepFormat.changeTitleLine('------------------') sepFormat.changeOutputLines(['
    ']) treeFormats[sepFormat.name] = sepFormat return treeFormats def importMozilla(self): """Import an HTML mozilla-format bookmark file. Return the structure if import is successful, otherwise None. """ structure = treestructure.TreeStructure() structure.treeFormats = self.createBookmarkFormat() with self.pathObj.open(encoding='utf-8') as f: text = f.read() try: handler = HtmlBookmarkHandler(structure) handler.feed(text) handler.close() except ValueError: return None structure.generateSpots(None) return structure def importXbel(self): """Import an XBEL format bookmark file. Return the structure if import is successful, otherwise None. """ structure = treestructure.TreeStructure() structure.treeFormats = self.createBookmarkFormat() tree = ElementTree.ElementTree() try: tree.parse(str(self.pathObj)) except ElementTree.ParseError: return None self.loadXbelNode(tree.getroot(), structure, None) if structure.childList: structure.generateSpots(None) return structure return None def loadXbelNode(self, element, structure, parent=None): """Recursively load an XBEL ElementTree node and its children. Arguments: element -- an XBEL ElementTree node model -- a ref to the TreeLine model parent -- the parent TreeNode (None for the root node only) """ if element.tag in ('xbel', 'folder'): node = treenode.TreeNode(structure. treeFormats[bookmarkFolderTypeName]) structure.addNodeDictRef(node) if parent: parent.childList.append(node) else: structure.childList.append(node) for child in element: self.loadXbelNode(child, structure, node) elif element.tag == 'bookmark': node = treenode.TreeNode(structure. treeFormats[bookmarkLinkTypeName]) structure.addNodeDictRef(node) parent.childList.append(node) link = element.get('href').strip() if link: node.data[bookmarkLinkFieldName] = ('{1}'. format(link, link)) for child in element: self.loadXbelNode(child, structure, node) elif element.tag == 'title': parent.setTitle(element.text) elif element.tag == 'separator': node = treenode.TreeNode(structure. treeFormats[bookmarkSeparatorTypeName]) structure.addNodeDictRef(node) parent.childList.append(node) else: # unsupported tags pass class HtmlBookmarkHandler(html.parser.HTMLParser): """Handler to parse HTML mozilla bookmark format. """ def __init__(self, structure): """Initialize the HTML parser object. Arguments: structure -- a reference to the tree structure """ super().__init__() self.structure = structure rootNode = treenode.TreeNode(self.structure. treeFormats[bookmarkFolderTypeName]) rootNode.data[nodeformat.defaultFieldName] = _('Bookmarks') self.structure.addNodeDictRef(rootNode) self.structure.childList = [rootNode] self.currentNode = rootNode self.parents = [] self.text = '' def handle_starttag(self, tag, attrs): """Called by the reader at each open tag. Arguments: tag -- the tag label attrs -- any tag attributes """ if tag == 'dt' or tag == 'h1': # start any entry self.text = '' elif tag == 'dl': # start indent self.parents.append(self.currentNode) self.currentNode = None elif tag == 'h3': # start folder if not self.parents: raise ValueError self.currentNode = treenode.TreeNode(self.structure. treeFormats[bookmarkFolderTypeName]) self.structure.addNodeDictRef(self.currentNode) self.parents[-1].childList.append(self.currentNode) elif tag == 'a': # start link if not self.parents: raise ValueError self.currentNode = treenode.TreeNode(self.structure. treeFormats[bookmarkLinkTypeName]) self.structure.addNodeDictRef(self.currentNode) self.parents[-1].childList.append(self.currentNode) for name, value in attrs: if name == 'href': link = '{0}'.format(value) self.currentNode.data[bookmarkLinkFieldName] = link elif tag == 'hr': # separator if not self.parents: raise ValueError node = treenode.TreeNode(self.structure. treeFormats[bookmarkSeparatorTypeName]) self.structure.addNodeDictRef(node) self.parents[-1].childList.append(node) self.currentNode = None def handle_endtag(self, tag): """Called by the reader at each end tag. Arguments: tag -- the tag label """ if tag == 'dl': # end indented section self.parents = self.parents[:-1] self.currentNode = None elif tag == 'h3' or tag == 'a': # end folder or link if not self.currentNode: raise ValueError self.currentNode.data[nodeformat.defaultFieldName] = self.text elif tag == 'h1': # end main title self.structure.childList[0].data[nodeformat. defaultFieldName] = self.text def handle_data(self, data): """Called by the reader to process text. Arguments: data -- the new text """ self.text += data def handle_entityref(self, name): """Convert escaped entity ref to char. Arguments: name -- the name of the escaped entity """ self.text += htmlUnescapeDict.get(name, '') TreeLine/source/treeline.spec0000644000175000017500000000554613716021700015235 0ustar dougdoug# -*- mode: python -*- #****************************************************************************** # treeline.spec, provides settings for use with PyInstaller # # Creates a standalone windows executable # # Run the build process by running the command 'pyinstaller treeline.spec' # # If everything works well you should find a 'dist/treeline' subdirectory # that contains the files needed to run the application # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** block_cipher = None extraFiles = [('../doc', 'doc'), ('../icons', 'icons'), ('../samples', 'samples'), ('../source/*.py', 'source'), ('../source/*.pro', 'source'), ('../source/*.spec', 'source'), ('../templates', 'templates'), ('../translations', 'translations'), ('../win/*.*', '.')] a = Analysis(['treeline.py'], pathex=['C:\\git\\treeline\\devel\\source'], binaries=[], datas=extraFiles, hiddenimports=[], hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, [], exclude_binaries=True, name='treeline', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, console=False, icon='..\\win\\treeline.ico') a.binaries = a.binaries - TOC([('d3dcompiler_47.dll', None, None), ('libcrypto-1_1.dll', None, None), ('libeay32.dll', None, None), ('libglesv2.dll', None, None), ('libssl-1_1.dll', None, None), ('opengl32sw.dll', None, None), ('qt5dbus.dll', None, None), ('qt5qml.dll', None, None), ('qt5qmlmodels.dll', None, None), ('qt5quick.dll', None, None), ('qt5websockets.dll', None, None)]) coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, name='treeline') TreeLine/source/treeview.py0000644000175000017500000007671413714637332014777 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treeview.py, provides a class for the indented tree view # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import unicodedata from PyQt5.QtCore import QEvent, QPoint, QPointF, Qt, pyqtSignal from PyQt5.QtGui import (QContextMenuEvent, QKeySequence, QMouseEvent, QTextDocument) from PyQt5.QtWidgets import (QAbstractItemView, QApplication, QHeaderView, QLabel, QListWidget, QListWidgetItem, QMenu, QStyledItemDelegate, QTreeView) import treeselection import treenode import miscdialogs import globalref class TreeView(QTreeView): """Class override for the indented tree view. Sets view defaults and links with document for content. """ skippedMouseSelect = pyqtSignal(treenode.TreeNode) shortcutEntered = pyqtSignal(QKeySequence) def __init__(self, model, allActions, parent=None): """Initialize the tree view. Arguments: model -- the initial model for view data allActions -- a dictionary of control actions for popup menus parent -- the parent main window """ super().__init__(parent) self.resetModel(model) self.allActions = allActions self.incremSearchMode = False self.incremSearchString = '' self.noMouseSelectMode = False self.mouseFocusNoEditMode = False self.prevSelSpot = None # temp, to check for edit at mouse release self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.header().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.header().setStretchLastSection(False) self.setHeaderHidden(True) self.setItemDelegate(TreeEditDelegate(self)) # use mouse event for editing to avoid with multiple select self.setEditTriggers(QAbstractItemView.NoEditTriggers) self.updateTreeGenOptions() self.setDragDropMode(QAbstractItemView.DragDrop) self.setDefaultDropAction(Qt.MoveAction) self.setDropIndicatorShown(True) self.setUniformRowHeights(True) def resetModel(self, model): """Change the model assigned to this view. Also assigns a new selection model. Arguments: model -- the new model to assign """ self.setModel(model) self.setSelectionModel(treeselection.TreeSelection(model, self)) def updateTreeGenOptions(self): """Set the tree to match the current general options. """ dragAvail = globalref.genOptions['DragTree'] self.setDragEnabled(dragAvail) self.setAcceptDrops(dragAvail) self.setIndentation(globalref.genOptions['IndentOffset'] * self.fontInfo().pixelSize()) def isSpotExpanded(self, spot): """Return True if the given spot is expanded (showing children). Arguments: spot -- the spot to check """ return self.isExpanded(spot.index(self.model())) def expandSpot(self, spot): """Expand a spot in this view. Arguments: spot -- the spot to expand """ self.expand(spot.index(self.model())) def collapseSpot(self, spot): """Collapse a spot in this view. Arguments: spot -- the spot to collapse """ self.collapse(spot.index(self.model())) def expandBranch(self, parentSpot): """Expand all spots in the given branch. Collapses parentSpot first to avoid extreme slowness. Arguments: parentSpot -- the top spot in the branch """ self.collapse(parentSpot.index(self.model())) for spot in parentSpot.spotDescendantOnlyGen(): if spot.nodeRef.childList: self.expand(spot.index(self.model())) self.expand(parentSpot.index(self.model())) def collapseBranch(self, parentSpot): """Collapse all spots in the given branch. Arguments: parentSpot -- the top spot in the branch """ for spot in parentSpot.spotDescendantGen(): if spot.nodeRef.childList: self.collapse(spot.index(self.model())) def savedExpandState(self, spots): """Return a list of tuples of spots and expanded state (True/False). Arguments: spots -- an iterable of spots to save """ return [(spot, self.isSpotExpanded(spot)) for spot in spots] def restoreExpandState(self, expandState): """Expand or collapse based on saved tuples. Arguments: expandState -- a list of tuples of spots and expanded state """ for spot, expanded in expandState: try: if expanded: self.expandSpot(spot) else: self.collapseSpot(spot) except ValueError: pass def spotAtTop(self): """If view is scrolled, return the spot at the top of the view. If not scrolled, return None. """ if self.verticalScrollBar().value() > 0: return self.indexAt(QPoint(0, 0)).internalPointer() return None def scrollToSpot(self, spot): """Scroll the view to move the spot to the top position. Arguments: spot -- the spot to move to the top """ self.scrollTo(spot.index(self.model()), QAbstractItemView.PositionAtTop) def scrollTo(self, index, hint=QAbstractItemView.EnsureVisible): """Scroll the view to make node at index visible. Overriden to stop autoScroll from horizontally jumping when selecting nodes. Arguments: index -- the node to be made visible hint -- where the visible item should be """ horizPos = self.horizontalScrollBar().value() super().scrollTo(index, hint) self.horizontalScrollBar().setValue(horizPos) def endEditing(self): """Stop the editing of any item being renamed. """ delegate = self.itemDelegate() if delegate.editor: delegate.commitData.emit(delegate.editor) self.closePersistentEditor(self.selectionModel().currentIndex()) def incremSearchStart(self): """Start an incremental title search. """ self.incremSearchMode = True self.incremSearchString = '' globalref.mainControl.currentStatusBar().showMessage(_('Search for:')) def incremSearchRun(self): """Perform an incremental title search. """ msg = _('Search for: {0}').format(self.incremSearchString) globalref.mainControl.currentStatusBar().showMessage(msg) if (self.incremSearchString and not self.selectionModel().selectTitleMatch(self.incremSearchString, True, True)): msg = _('Search for: {0} (not found)').format(self. incremSearchString) globalref.mainControl.currentStatusBar().showMessage(msg) def incremSearchNext(self): """Go to the next match in an incremental title search. """ if self.incremSearchString: if self.selectionModel().selectTitleMatch(self.incremSearchString): msg = _('Next: {0}').format(self.incremSearchString) else: msg = _('Next: {0} (not found)').format(self. incremSearchString) globalref.mainControl.currentStatusBar().showMessage(msg) def incremSearchPrev(self): """Go to the previous match in an incremental title search. """ if self.incremSearchString: if self.selectionModel().selectTitleMatch(self.incremSearchString, False): msg = _('Next: {0}').format(self.incremSearchString) else: msg = _('Next: {0} (not found)').format(self. incremSearchString) globalref.mainControl.currentStatusBar().showMessage(msg) def incremSearchStop(self): """End an incremental title search. """ self.incremSearchMode = False self.incremSearchString = '' globalref.mainControl.currentStatusBar().clearMessage() def showTypeMenu(self, menu): """Show a popup menu for setting the item type. """ index = self.selectionModel().currentIndex() self.scrollTo(index) rect = self.visualRect(index) pt = self.mapToGlobal(QPoint(rect.center().x(), rect.bottom())) menu.popup(pt) def contextMenu(self): """Return the context menu, creating it if necessary. """ menu = QMenu(self) menu.addAction(self.allActions['EditCut']) menu.addAction(self.allActions['EditCopy']) menu.addAction(self.allActions['EditPaste']) menu.addAction(self.allActions['NodeRename']) menu.addSeparator() menu.addAction(self.allActions['NodeInsertBefore']) menu.addAction(self.allActions['NodeInsertAfter']) menu.addAction(self.allActions['NodeAddChild']) menu.addSeparator() menu.addAction(self.allActions['NodeDelete']) menu.addAction(self.allActions['NodeIndent']) menu.addAction(self.allActions['NodeUnindent']) menu.addSeparator() menu.addAction(self.allActions['NodeMoveUp']) menu.addAction(self.allActions['NodeMoveDown']) menu.addSeparator() menu.addMenu(self.allActions['DataNodeType'].parent()) menu.addSeparator() menu.addAction(self.allActions['ViewExpandBranch']) menu.addAction(self.allActions['ViewCollapseBranch']) return menu def contextMenuEvent(self, event): """Show popup context menu on mouse click or menu key. Arguments: event -- the context menu event """ if event.reason() == QContextMenuEvent.Mouse: clickedSpot = self.indexAt(event.pos()).internalPointer() if not clickedSpot: event.ignore() return if clickedSpot not in self.selectionModel().selectedSpots(): self.selectionModel().selectSpots([clickedSpot]) pos = event.globalPos() else: # shown for menu key or other reason selectList = self.selectionModel().selectedSpots() if not selectList: event.ignore() return currentSpot = self.selectionModel().currentSpot() if currentSpot in selectList: selectList.insert(0, currentSpot) position = None for spot in selectList: rect = self.visualRect(spot.index(self.model())) pt = QPoint(rect.center().x(), rect.bottom()) if self.rect().contains(pt): position = pt break if not position: self.scrollTo(selectList[0].index(self.model())) rect = self.visualRect(selectList[0].index(self.model())) position = QPoint(rect.center().x(), rect.bottom()) pos = self.mapToGlobal(position) self.contextMenu().popup(pos) event.accept() def dropEvent(self, event): """Event handler for view drop actions. Selects parent node at destination. Arguments: event -- the drop event """ clickedSpot = self.indexAt(event.pos()).internalPointer() # clear selection to avoid invalid multiple selection bug self.selectionModel().selectSpots([], False) if clickedSpot: super().dropEvent(event) self.selectionModel().selectSpots([clickedSpot], False) self.scheduleDelayedItemsLayout() # reqd before expand self.expandSpot(clickedSpot) else: super().dropEvent(event) self.selectionModel().selectSpots([]) self.scheduleDelayedItemsLayout() if event.isAccepted(): self.model().treeModified.emit(True, True) def toggleNoMouseSelectMode(self, active=True): """Set noMouseSelectMode to active or inactive. noMouseSelectMode will not change selection on mouse click, it will just signal the clicked node for use in links, etc. Arguments: active -- if True, activate noMouseSelectMode """ self.noMouseSelectMode = active def clearHover(self): """Post a mouse move event to clear the mouse hover indication. Needed to avoid crash when deleting nodes with hovered child nodes. """ event = QMouseEvent(QEvent.MouseMove, QPointF(0.0, self.viewport().width()), Qt.NoButton, Qt.NoButton, Qt.NoModifier) QApplication.postEvent(self.viewport(), event) QApplication.processEvents() def mousePressEvent(self, event): """Skip unselecting click if in noMouseSelectMode. If in noMouseSelectMode, signal which node is under the mouse. Arguments: event -- the mouse click event """ if self.incremSearchMode: self.incremSearchStop() self.prevSelSpot = None clickedIndex = self.indexAt(event.pos()) clickedSpot = clickedIndex.internalPointer() selectModel = self.selectionModel() if self.noMouseSelectMode: if clickedSpot and event.button() == Qt.LeftButton: self.skippedMouseSelect.emit(clickedSpot.nodeRef) event.ignore() return if (event.button() == Qt.LeftButton and not self.mouseFocusNoEditMode and selectModel.selectedCount() == 1 and selectModel.currentSpot() == selectModel.selectedSpots()[0] and event.pos().x() > self.visualRect(clickedIndex).left() and globalref.genOptions['ClickRename']): # set for edit if single select and not an expand/collapse click self.prevSelSpot = selectModel.selectedSpots()[0] self.mouseFocusNoEditMode = False super().mousePressEvent(event) def mouseReleaseEvent(self, event): """Initiate editing if clicking on a single selected node. Arguments: event -- the mouse click event """ clickedIndex = self.indexAt(event.pos()) clickedSpot = clickedIndex.internalPointer() if (event.button() == Qt.LeftButton and self.prevSelSpot and clickedSpot == self.prevSelSpot): self.edit(clickedIndex) event.ignore() return self.prevSelSpot = None super().mouseReleaseEvent(event) def keyPressEvent(self, event): """Record characters if in incremental search mode. Arguments: event -- the key event """ if self.incremSearchMode: if event.key() in (Qt.Key_Return, Qt.Key_Enter, Qt.Key_Escape): self.incremSearchStop() elif event.key() == Qt.Key_Backspace and self.incremSearchString: self.incremSearchString = self.incremSearchString[:-1] self.incremSearchRun() elif event.text() and unicodedata.category(event.text()) != 'Cc': # unicode category excludes control characters self.incremSearchString += event.text() self.incremSearchRun() event.accept() elif (event.key() in (Qt.Key_Return, Qt.Key_Enter) and not self.itemDelegate().editor): # enter key selects current item if not selected selectModel = self.selectionModel() if selectModel.currentSpot() not in selectModel.selectedSpots(): selectModel.selectSpots([selectModel.currentSpot()]) event.accept() else: super().keyPressEvent(event) else: super().keyPressEvent(event) def focusInEvent(self, event): """Avoid editing a tree item with a get-focus click. Arguments: event -- the focus in event """ if event.reason() == Qt.MouseFocusReason: self.mouseFocusNoEditMode = True super().focusInEvent(event) def focusOutEvent(self, event): """Stop incremental search on focus loss. Arguments: event -- the focus out event """ if self.incremSearchMode: self.incremSearchStop() super().focusOutEvent(event) class TreeEditDelegate(QStyledItemDelegate): """Class override for editing tree items to capture shortcut keys. """ def __init__(self, parent=None): """Initialize the delegate class. Arguments: parent -- the parent view """ super().__init__(parent) self.editor = None def createEditor(self, parent, styleOption, modelIndex): """Return a new text editor for an item. Arguments: parent -- the parent widget for the editor styleOption -- the data for styles and geometry modelIndex -- the index of the item to be edited """ self.editor = super().createEditor(parent, styleOption, modelIndex) return self.editor def destroyEditor(self, editor, index): """Reset editor storage after editing ends. Arguments: editor -- the editor that is ending index -- the index of the edited item """ self.editor = None super().destroyEditor(editor, index) def eventFilter(self, editor, event): """Override to handle shortcut control keys. Arguments: editor -- the editor that Qt installed a filter on event -- the key press event """ if (event.type() == QEvent.KeyPress and event.modifiers() == Qt.ControlModifier and Qt.Key_A <= event.key() <= Qt.Key_Z): key = QKeySequence(event.modifiers() | event.key()) self.parent().shortcutEntered.emit(key) return True return super().eventFilter(editor, event) class TreeFilterViewItem(QListWidgetItem): """Item container for the flat list of filtered nodes. """ def __init__(self, spot, viewParent=None): """Initialize the list view item. Arguments: spot -- the spot to reference for content viewParent -- the parent list view """ super().__init__(viewParent) self.spot = spot self.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled) self.update() def update(self): """Update title and icon from the stored node. """ node = self.spot.nodeRef self.setText(node.title()) if globalref.genOptions['ShowTreeIcons']: self.setIcon(globalref.treeIcons.getIcon(node.formatRef.iconName, True)) class TreeFilterView(QListWidget): """View to show flat list of filtered nodes. """ skippedMouseSelect = pyqtSignal(treenode.TreeNode) shortcutEntered = pyqtSignal(QKeySequence) def __init__(self, treeViewRef, allActions, parent=None): """Initialize the list view. Arguments: treeViewRef -- a ref to the tree view for data allActions -- a dictionary of control actions for popup menus parent -- the parent main window """ super().__init__(parent) self.structure = treeViewRef.model().treeStructure self.selectionModel = treeViewRef.selectionModel() self.treeModel = treeViewRef.model() self.allActions = allActions self.menu = None self.noMouseSelectMode = False self.mouseFocusNoEditMode = False self.prevSelSpot = None # temp, to check for edit at mouse release self.drivingSelectionChange = False self.conditionalFilter = None self.messageLabel = None self.filterWhat = miscdialogs.FindScope.fullData self.filterHow = miscdialogs.FindType.keyWords self.filterStr = '' self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setItemDelegate(TreeEditDelegate(self)) # use mouse event for editing to avoid with multiple select self.setEditTriggers(QAbstractItemView.NoEditTriggers) self.itemSelectionChanged.connect(self.updateSelectionModel) self.itemChanged.connect(self.changeTitle) treeFont = QTextDocument().defaultFont() treeFontName = globalref.miscOptions['TreeFont'] if treeFontName: treeFont.fromString(treeFontName) self.setFont(treeFont) def updateItem(self, node): """Update the item corresponding to the given node. Arguments: node -- the node to be updated """ for row in range(self.count()): if self.item(row).spot.nodeRef == node: self.blockSignals(True) self.item(row).update() self.blockSignals(False) return def updateContents(self): """Update filtered contents from current structure and filter criteria. """ if self.conditionalFilter: self.conditionalUpdate() return QApplication.setOverrideCursor(Qt.WaitCursor) if self.filterHow == miscdialogs.FindType.regExp: criteria = [re.compile(self.filterStr)] useRegExpFilter = True elif self.filterHow == miscdialogs.FindType.fullWords: criteria = [] for word in self.filterStr.lower().split(): criteria.append(re.compile(r'(?i)\b{}\b'. format(re.escape(word)))) useRegExpFilter = True elif self.filterHow == miscdialogs.FindType.keyWords: criteria = self.filterStr.lower().split() useRegExpFilter = False else: # full phrase criteria = [self.filterStr.lower().strip()] useRegExpFilter = False titlesOnly = self.filterWhat == miscdialogs.FindScope.titlesOnly self.blockSignals(True) self.clear() if useRegExpFilter: for rootSpot in self.structure.rootSpots(): for spot in rootSpot.spotDescendantGen(): if spot.nodeRef.regExpSearch(criteria, titlesOnly): item = TreeFilterViewItem(spot, self) else: for rootSpot in self.structure.rootSpots(): for spot in rootSpot.spotDescendantGen(): if spot.nodeRef.wordSearch(criteria, titlesOnly): item = TreeFilterViewItem(spot, self) self.blockSignals(False) self.selectItems(self.selectionModel.selectedSpots(), True) if self.count() and not self.selectedItems(): self.item(0).setSelected(True) if not self.messageLabel: self.messageLabel = QLabel() globalref.mainControl.currentStatusBar().addWidget(self. messageLabel) message = _('Filtering by "{0}", found {1} nodes').format(self. filterStr, self.count()) self.messageLabel.setText(message) QApplication.restoreOverrideCursor() def conditionalUpdate(self): """Update filtered contents from structure and conditional criteria. """ QApplication.setOverrideCursor(Qt.WaitCursor) self.blockSignals(True) self.clear() for rootSpot in self.structure.rootSpots(): for spot in rootSpot.spotDescendantGen(): if self.conditionalFilter.evaluate(spot.nodeRef): item = TreeFilterViewItem(spot, self) self.blockSignals(False) self.selectItems(self.selectionModel.selectedSpots(), True) if self.count() and not self.selectedItems(): self.item(0).setSelected(True) if not self.messageLabel: self.messageLabel = QLabel() globalref.mainControl.currentStatusBar().addWidget(self. messageLabel) message = _('Conditional filtering, found {0} nodes').format(self. count()) self.messageLabel.setText(message) QApplication.restoreOverrideCursor() def selectItems(self, spots, signalModel=False): """Select items matching given nodes if in filtered view. Arguments: spots -- the spot list to select signalModel -- signal to update the tree selection model if True """ selectSpots = set(spots) if not signalModel: self.blockSignals(True) for item in self.selectedItems(): item.setSelected(False) for row in range(self.count()): if self.item(row).spot in selectSpots: self.item(row).setSelected(True) self.setCurrentItem(self.item(row)) self.blockSignals(False) def updateFromSelectionModel(self): """Select items selected in the tree selection model. Called from a signal that the tree selection model is changing. """ if self.count() and not self.drivingSelectionChange: self.selectItems(self.selectionModel.selectedSpots()) def updateSelectionModel(self): """Change the selection model based on a filter list selection signal. """ self.drivingSelectionChange = True self.selectionModel.selectSpots([item.spot for item in self.selectedItems()]) self.drivingSelectionChange = False def changeTitle(self, item): """Update the node title in the model based on an edit signal. Reset to the node text if invalid. Arguments: item -- the filter view item that changed """ if not self.treeModel.setData(item.spot.index(self.treeModel), item.text()): self.blockSignals(True) item.setText(item.node.title()) self.blockSignals(False) def nextPrevSpot(self, spot, forward=True): """Return the next or previous spot in this filter list view. Wraps around ends. Return None if view doesn't have spot. Arguments: spot -- the starting spot forward -- next if True, previous if False """ for row in range(self.count()): if self.item(row).spot == spot: if forward: row += 1 if row >= self.count(): row = 0 else: row -= 1 if row < 0: row = self.count() - 1 return self.item(row).spot return None def contextMenu(self): """Return the context menu, creating it if necessary. """ if not self.menu: self.menu = QMenu(self) self.menu.addAction(self.allActions['EditCut']) self.menu.addAction(self.allActions['EditCopy']) self.menu.addAction(self.allActions['NodeRename']) self.menu.addSeparator() self.menu.addAction(self.allActions['NodeDelete']) self.menu.addSeparator() self.menu.addMenu(self.allActions['DataNodeType'].parent()) return self.menu def contextMenuEvent(self, event): """Show popup context menu on mouse click or menu key. Arguments: event -- the context menu event """ if event.reason() == QContextMenuEvent.Mouse: clickedItem = self.itemAt(event.pos()) if not clickedItem: event.ignore() return if clickedItem.spot not in self.selectionModel.selectedSpots(): self.selectionModel.selectSpots([clickedItem.spot]) pos = event.globalPos() else: # shown for menu key or other reason selectList = self.selectedItems() if not selectList: event.ignore() return currentItem = self.currentItem() if currentItem in selectList: selectList.insert(0, currentItem) posList = [] for item in selectList: rect = self.visualItemRect(item) pt = QPoint(rect.center().x(), rect.bottom()) if self.rect().contains(pt): posList.append(pt) if not posList: self.scrollTo(self.indexFromItem(selectList[0])) rect = self.visualItemRect(selectList[0]) posList = [QPoint(rect.center().x(), rect.bottom())] pos = self.mapToGlobal(posList[0]) self.contextMenu().popup(pos) event.accept() def toggleNoMouseSelectMode(self, active=True): """Set noMouseSelectMode to active or inactive. noMouseSelectMode will not change selection on mouse click, it will just signal the clicked node for use in links, etc. Arguments: active -- if True, activate noMouseSelectMode """ self.noMouseSelectMode = active def mousePressEvent(self, event): """Skip unselecting click on blank spaces. Arguments: event -- the mouse click event """ self.prevSelSpot = None clickedItem = self.itemAt(event.pos()) if not clickedItem: event.ignore() return if self.noMouseSelectMode: if event.button() == Qt.LeftButton: self.skippedMouseSelect.emit(clickedItem.spot.nodeRef) event.ignore() return if (event.button() == Qt.LeftButton and not self.mouseFocusNoEditMode and self.selectionModel.selectedCount() == 1 and globalref.genOptions['ClickRename']): self.prevSelSpot = self.selectionModel.selectedSpots()[0] self.mouseFocusNoEditMode = False super().mousePressEvent(event) def mouseReleaseEvent(self, event): """Initiate editing if clicking on a single selected node. Arguments: event -- the mouse click event """ clickedItem = self.itemAt(event.pos()) if (event.button() == Qt.LeftButton and clickedItem and self.prevSelSpot and clickedItem.spot == self.prevSelSpot): self.editItem(clickedItem) event.ignore() return self.prevSelSpot = None super().mouseReleaseEvent(event) def focusInEvent(self, event): """Avoid editing a tree item with a get-focus click. Arguments: event -- the focus in event """ if event.reason() == Qt.MouseFocusReason: self.mouseFocusNoEditMode = True super().focusInEvent(event) TreeLine/source/options.py0000644000175000017500000006560213702667437014640 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # options.py, provides a class to manage config options # # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** from collections import OrderedDict import sys import re import pathlib import os.path import json from PyQt5.QtCore import Qt from PyQt5.QtGui import QKeySequence from PyQt5.QtWidgets import (QButtonGroup, QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QRadioButton, QSpinBox, QVBoxLayout) import miscdialogs multipleSpaceRegEx = re.compile(r' {2,}') class StringOptionItem: """Class to store and control a string-based config option entry. Stores the name, value, category and description, provides validation, config file output and dialog controls. """ def __init__(self, optionDict, name, value, emptyOK=True, stripSpaces=False, category='', description='', columnNum=0): """Set the parameters and initial value and add to optionDict. Raises a ValueError if initial validation fails. Arguments: optionDict -- a dictionary to add this option item to name -- the string key for the option value -- the string value emptyOK -- if False, does not allow empty string stripSpaces -- if True, remove leading, trailing & double spaces category -- a string for the option group this belongs to description -- a string for use in the control dialog columnNum -- the column position for this control in the dialog """ self.name = name self.category = category self.description = description self.columnNum = columnNum self.emptyOK = emptyOK self.stripSpaces = stripSpaces self.dialogControl = None self.value = None self.setValue(value) self.defaultValue = self.value optionDict[name] = self def setValue(self, value): """Sets the value and validates, returns True if OK. Returns False if validation fails but the old value is OK, or if the value is unchanged. Raises a ValueError if validation fails without an old value. Arguments: value -- the string value to set """ value = str(value) if self.stripSpaces: value = multipleSpaceRegEx.sub(' ', value.strip()) if value != self.value and (value or self.emptyOK): self.value = value return True if self.value == None: raise ValueError return False def storedValue(self): """Return the value to be stored in the JSON file. """ return self.value def addDialogControl(self, groupBox): """Add the labels and controls to a dialog box for this option item. Arguments: groupBox -- the current group box """ gridLayout = groupBox.layout() row = gridLayout.rowCount() label = QLabel(self.description, groupBox) gridLayout.addWidget(label, row, 0) self.dialogControl = QLineEdit(self.value, groupBox) gridLayout.addWidget(self.dialogControl, row, 1) def updateFromDialog(self): """Set the value of this item from the dialog contents. Return True if successfully set. """ return self.setValue(self.dialogControl.text()) class IntOptionItem(StringOptionItem): """Class to store and control an integer-based config option entry. Stores the name, value, category and description, provides validation, config file output and dialog controls. """ def __init__(self, optionDict, name, value, minimum=None, maximum=None, category='', description='', columnNum=0): """Set the parameters and initial value and add to optionDict. Raises a ValueError if initial validation fails. Arguments: optionDict -- a dictionary to add this option item to name -- the string key for the option value -- a numeric or string-based value minimum -- optional minimum value maximum -- optional maximum value category -- a string for the option group this belongs to description -- a string for use in the control dialog columnNum -- the column position for this control in the dialog """ self.minimum = minimum self.maximum = maximum super().__init__(optionDict, name, value, False, False, category, description, columnNum) def setValue(self, value): """Sets the value and validates, returns True if OK. Returns False if validation fails but the old value is OK, or if the value is unchanged. Raises a ValueError if validation fails without an old value. Arguments: value -- a numeric or string-based value to set """ try: value = int(value) if self.minimum != None and value < self.minimum: raise ValueError if self.maximum != None and value > self.maximum: raise ValueError except ValueError: if self.value == None: raise return False if value != self.value: self.value = value return True return False def addDialogControl(self, groupBox): """Add the labels and controls to a dialog box for this option item. Arguments: groupBox -- the current group box """ gridLayout = groupBox.layout() row = gridLayout.rowCount() label = QLabel(self.description, groupBox) gridLayout.addWidget(label, row, 0) self.dialogControl = QSpinBox(groupBox) self.dialogControl.setValue(self.value) if self.minimum != None: self.dialogControl.setMinimum(self.minimum) if self.maximum != None: self.dialogControl.setMaximum(self.maximum) gridLayout.addWidget(self.dialogControl, row, 1) def updateFromDialog(self): """Set the value of this item from the dialog contents. Return True if successfully set. """ return self.setValue(self.dialogControl.value()) class FloatOptionItem(StringOptionItem): """Class to store and control a float-based config option entry. Stores the name, value, category and description, provides validation, config file output and dialog controls. """ def __init__(self, optionDict, name, value, minimum=None, maximum=None, category='', description='', columnNum=0): """Set the parameters and initial value and add to optionDict. Raises a ValueError if initial validation fails. Arguments: optionDict -- a dictionary to add this option item to name -- the string key for the option value -- a numeric or string-based value minimum -- optional minimum value maximum -- optional maximum value category -- a string for the option group this belongs to description -- a string for use in the control dialog columnNum -- the column position for this control in the dialog """ self.minimum = minimum self.maximum = maximum super().__init__(optionDict, name, value, False, False, category, description, columnNum) def setValue(self, value): """Sets the value and validates, returns True if OK. Returns False if validation fails but the old value is OK, or if the value is unchanged. Raises a ValueError if validation fails without an old value. Arguments: value -- a numeric or string-based value to set """ try: value = float(value) if self.minimum != None and value < self.minimum: raise ValueError if self.maximum != None and value > self.maximum: raise ValueError except ValueError: if self.value == None: raise return False if value != self.value: self.value = value return True return False def addDialogControl(self, groupBox): """Add the labels and controls to a dialog box for this option item. Arguments: groupBox -- the current group box """ gridLayout = groupBox.layout() row = gridLayout.rowCount() label = QLabel(self.description, groupBox) gridLayout.addWidget(label, row, 0) self.dialogControl = QDoubleSpinBox(groupBox) self.dialogControl.setValue(self.value) if self.minimum != None: self.dialogControl.setMinimum(self.minimum) if self.maximum != None: self.dialogControl.setMaximum(self.maximum) gridLayout.addWidget(self.dialogControl, row, 1) def updateFromDialog(self): """Set the value of this item from the dialog contents. Return True if successfully set. """ return self.setValue(self.dialogControl.value()) class BoolOptionItem(StringOptionItem): """Class to store and control a boolean config option entry. Stores the name, value, category and description, provides validation, config file output and dialog controls. """ def __init__(self, optionDict, name, value, category='', description='', columnNum=0): """Set the parameters and initial value and add to optionDict. Raises a ValueError if initial validation fails. Arguments: optionDict -- a dictionary to add this option item to name -- the string key for the option value -- the boolean or string value category -- a string for the option group this belongs to description -- a string for use in the control dialog columnNum -- the column position for this control in the dialog """ super().__init__(optionDict, name, value, False, False, category, description, columnNum) def setValue(self, value): """Sets the value and validates, returns True if OK. Returns False if validation fails but the old value is OK, or if the value is unchanged. Raises a ValueError if validation fails without an old value. Arguments: value -- a boolean or string-based value to set """ if hasattr(value, 'lower'): if value.lower() in ('yes', 'y', 'true'): value = True elif value.lower() in ('no', 'n', 'false'): value = False else: value = bool(value) if value in (True, False) and value != self.value: self.value = value return True if self.value == None: raise ValueError return False def addDialogControl(self, groupBox): """Add the labels and controls to a dialog box for this option item. Arguments: groupBox -- the current group box """ gridLayout = groupBox.layout() row = gridLayout.rowCount() self.dialogControl = QCheckBox(self.description, groupBox) self.dialogControl.setChecked(self.value) gridLayout.addWidget(self.dialogControl, row, 0, 1, 2) def updateFromDialog(self): """Set the value of this item from the dialog contents. Return True if successfully set. """ return self.setValue(self.dialogControl.isChecked()) class ListOptionItem(StringOptionItem): """Class to store and control a pull-down list config option entry. Stores the name, value, category and description, provides validation, config file output and dialog controls. """ def __init__(self, optionDict, name, value, choices, category='', description='', columnNum=0): """Set the parameters and initial value and add to optionDict. Raises a ValueError if initial validation fails. Arguments: optionDict -- a dictionary to add this option item to name -- the string key for the option value -- the string value choices -- a list of acceptable entries category -- a string for the option group this belongs to description -- a string for use in the control dialog columnNum -- the column position for this control in the dialog """ self.choices = choices super().__init__(optionDict, name, value, False, False, category, description, columnNum) def setValue(self, value): """Sets the value and validates, returns True if OK. Returns False if validation fails but the old value is OK, or if the value is unchanged. Raises a ValueError if validation fails without an old value. Arguments: value -- the string value to set """ value = str(value) if value in self.choices and value != self.value: self.value = value return True if self.value == None: raise ValueError return False def addDialogControl(self, groupBox): """Add the labels and controls to a dialog box for this option item. Arguments: groupBox -- the current group box """ gridLayout = groupBox.layout() row = gridLayout.rowCount() label = QLabel(self.description, groupBox) gridLayout.addWidget(label, row, 0) self.dialogControl = QComboBox(groupBox) self.dialogControl.addItems(self.choices) self.dialogControl.setCurrentIndex(self.choices.index(self.value)) gridLayout.addWidget(self.dialogControl, row, 1) def updateFromDialog(self): """Set the value of this item from the dialog contents. Return True if successfully set. """ return self.setValue(self.dialogControl.currentText()) class ChoiceOptionItem(StringOptionItem): """Class to store and control a radio button choice config option entry. Stores the name, value, category and description, provides validation, config file output and dialog controls. """ def __init__(self, optionDict, name, value, choices, category='', columnNum=0): """Set the parameters and initial value and add to optionDict. Raises a ValueError if initial validation fails. Arguments: optionDict -- a dictionary to add this option item to name -- the string key for the option value -- the string value choices -- a list of acceptable entries category -- a string for the option group this belongs to columnNum -- the column position for this control in the dialog """ self.choices = choices super().__init__(optionDict, name, value, False, False, category, '', columnNum) def setValue(self, value): """Sets the value and validates, returns True if OK. Returns False if validation fails but the old value is OK, or if the value is unchanged. Raises a ValueError if validation fails without an old value. Arguments: value -- the string value to set """ value = str(value) if value in self.choices and value != self.value: self.value = value return True if self.value == None: raise ValueError return False def addDialogControl(self, groupBox): """Add the labels and controls to a dialog box for this option item. Arguments: groupBox -- the current group box """ rowLayout.addWidget(groupBox) QGridLayout(groupBox) gridLayout = groupBox.layout() self.dialogControl = QButtonGroup(groupBox) row = 0 for choice in self.choices: button = QRadioButton(choice, groupBox) self.dialogControl.addButton(button) gridLayout.addWidget(button, row, 0, 1, 2) row += 1 def updateFromDialog(self): """Set the value of this item from the dialog contents. Return True if successfully set. """ return self.setValue(self.dialogControl.checkedButton().text()) class KeyOptionItem(StringOptionItem): """Class to store and control a keyboard key based config option entry. Stores the name, value and category, provides validation and config file output. """ def __init__(self, optionDict, name, value, category=''): """Set the parameters and initial value and add to optionDict. Raises a ValueError if initial validation fails. Arguments: optionDict -- a dictionary to add this option item to name -- the string key for the option value -- a numeric or string-based value category -- a string for the option group this belongs to """ super().__init__(optionDict, name, value, True, False, category) def setValue(self, value): """Sets the value and validates, returns True if OK. Returns False if validation fails but the old value is OK, or if the value is unchanged. Raises a ValueError if validation fails without an old value. Arguments: value -- a key string value to set """ key = QKeySequence(value) if value and key.isEmpty(): if self.value == None: raise ValueError return False if value != self.value: self.value = key return True return False def storedValue(self): """Return the value to be stored in the JSON file. """ return self.value.toString() class DataListOptionItem(StringOptionItem): """Class to store an arbitrary length list containing various data. Stores the name and value, provides validation and config file output. """ def __init__(self, optionDict, name, value): """Set the parameters and initial value and add to optionDict. Raises a ValueError if initial validation fails. Arguments: optionDict -- a dictionary to add this option item to name -- the string key for the option value -- a list containg other basic data types """ super().__init__(optionDict, name, value) def setValue(self, value): """Sets the value and validates, returns True if OK. Returns False if validation fails but the old value is OK, or if the value is unchanged. Raises a ValueError if validation fails without an old value. Arguments: value -- a list containg other basic data types """ if isinstance(value, list) and value != self.value: self.value = value return True if self.value == None: raise ValueError return False class Options(OrderedDict): """Class to store and control the config options for a program. """ basePath = None def __init__(self, fileName='', progName='', version='', coDirName=''): """Initialize and set the path to the config file. Creates the path dir structure if necessary (if fileName is given). On Windows, uses the module path's config directory if it exists. Arguments: fileName -- the config file name, excluding the extension progName -- the program name, for dialog headings & config dir name version -- a version string, for config dir names coDirName -- the company name for the config dir in Windows OS """ super().__init__() self.modified = False self.path = pathlib.Path() if not fileName: return # no storage without fileName (temporary options only) if not version: version = '0' appDirName = '{0}-{1}'.format(progName.lower(), version) fileNameSuffix = '.ini' if sys.platform.startswith('win') else 'rc' if not Options.basePath and progName and coDirName: if sys.platform.startswith('win'): # Windows userPath = (pathlib.Path(os.environ.get('APPDATA', '')) / coDirName / appDirName) else: # Linux, etc. userPath = (pathlib.Path(os.path.expanduser('~')) / ('.' + appDirName)) if userPath.is_dir(): Options.basePath = userPath else: # use abspath() - pathlib's resolve() buggy with network drives modPath = pathlib.Path(os.path.abspath(sys.path[0])) if modPath.is_file(): modPath = modPath.parent # for frozen binary modConfigPath = modPath / 'config' if modConfigPath.is_dir(): Options.basePath = modConfigPath elif os.access(str(modPath), os.W_OK): dialog = miscdialogs.RadioChoiceDialog(progName, _('Choose configuration file location'), [(_('User\'s home directory (recommended)'), 0), (_('Program directory (for portable use)'), 1)]) if dialog.exec_() != QDialog.Accepted: sys.exit(0) if dialog.selectedButton() == 1: Options.basePath = modConfigPath if not Options.basePath: Options.basePath = userPath try: if not Options.basePath.is_dir(): Options.basePath.mkdir(parents=True) iconPath = Options.basePath / 'icons' if not iconPath.is_dir(): iconPath.mkdir() templatePath = Options.basePath / 'templates' if not templatePath.is_dir(): templatePath.mkdir() templateExportPath = templatePath / 'exports' if not templateExportPath.is_dir(): templateExportPath.mkdir() except OSError: Options.basePath = None if Options.basePath: self.path = Options.basePath / (fileName + fileNameSuffix) def __getitem__(self, name): """Return the value of an option when called as options[name]. Arguments: name -- the string key for the option """ return self.get(name).value def getDefaultValue(self, name): """Return the initially set default value from the given option key. Arguments: name -- the string key for the option """ return self.get(name).defaultValue def changeValue(self, name, value): """Set a new value for the given option key. Return True if sucessful. Arguments: name -- the string key for the option value -- a value or a string defining the value """ if self.get(name).setValue(value): self.modified = True return True return False def resetToDefaults(self, keyList): """Reset all options with the given keys to original default values. Arguments: keyList -- a list of option names to reset """ for key in keyList: self.get(key).setValue(self.get(key).defaultValue) self.modified = True def removeValue(self, name): """Remove the value from the option list if possible. If not, fail silently. Arguments: name -- the string key for the option to be removed """ try: del self[name] except KeyError: return self.modified = True def readFile(self): """Read config options from the file on self.path. Create the file if it isn't found, raise IOError if this fails. Only updates existing config items. """ try: with self.path.open('r', encoding='utf-8') as f: data = json.load(f) for key, value in data.items(): try: self.get(key).setValue(value) except AttributeError: pass except (IOError, ValueError): if not self.writeFile(): raise IOError def writeFile(self): """Write current options to the file on self.path. Returns False on failure. """ try: with self.path.open('w', encoding='utf-8') as f: data = OrderedDict([(key, obj.storedValue()) for (key, obj) in self.items()]) json.dump(data, f, indent=0) self.modified = False except IOError: return False return True class OptionDialog(QDialog): """Provides a dialog with controls for all options in an Options instance. """ def __init__(self, options, parent=None): super().__init__(parent) self.options = options self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) topLayout = QVBoxLayout(self) self.setLayout(topLayout) columnLayout = QHBoxLayout() topLayout.addLayout(columnLayout) rowLayout = QVBoxLayout() columnLayout.addLayout(rowLayout) groupBox = None for option in self.options.values(): if option.columnNum > columnLayout.count() - 1: rowLayout = QVBoxLayout() columnLayout.addLayout(rowLayout) if not groupBox or groupBox.title() != option.category: groupBox = QGroupBox(option.category) rowLayout.addWidget(groupBox) QGridLayout(groupBox) option.addDialogControl(groupBox) ctrlLayout = QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) def accept(self): """Updates the options from the controls when the OK button is pressed. """ for option in self.options.values(): if option.updateFromDialog(): self.options.modified = True super().accept() TreeLine/source/fieldformat.py0000644000175000017500000027472513745103647015444 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # fieldformat.py, provides a class to handle field format types # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import sys import enum import datetime import xml.sax.saxutils as saxutils import gennumber import genboolean import numbering import matheval import urltools import globalref fieldTypes = [N_('Text'), N_('HtmlText'), N_('OneLineText'), N_('SpacedText'), N_('Number'), N_('Math'), N_('Numbering'), N_('Date'), N_('Time'), N_('DateTime'), N_('Boolean'), N_('Choice'), N_('AutoChoice'), N_('Combination'), N_('AutoCombination'), N_('ExternalLink'), N_('InternalLink'), N_('Picture'), N_('RegularExpression')] translatedFieldTypes = [_(name) for name in fieldTypes] _errorStr = '#####' _dateStampString = _('Now') _timeStampString = _('Now') MathResult = enum.Enum('MathResult', 'number date time boolean text') _mathResultBlank = {MathResult.number: 0, MathResult.date: 0, MathResult.time: 0, MathResult.boolean: False, MathResult.text: ''} _multipleSpaceRegEx = re.compile(r' {2,}') _lineBreakRegEx = re.compile(r'', re.I) _stripTagRe = re.compile(r'<.*?>') linkRegExp = re.compile(r']*href="([^"]+)"[^>]*>(.*?)', re.I | re.S) linkSeparateNameRegExp = re.compile(r'(.*) \[(.*)\]\s*$') _imageRegExp = re.compile(r']*src="([^"]+)"[^>]*>', re.I | re.S) class TextField: """Class to handle a rich-text field format type. Stores options and format strings for a text field type. Provides methods to return formatted data. """ typeName = 'Text' defaultFormat = '' showRichTextInCell = True evalHtmlDefault = False fixEvalHtmlSetting = True defaultNumLines = 1 editorClassName = 'RichTextEditor' sortTypeStr = '80_text' supportsInitDefault = True formatHelpMenuList = [] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ self.name = name if not formatData: formatData = {} self.prefix = formatData.get('prefix', '') self.suffix = formatData.get('suffix', '') self.initDefault = formatData.get('init', '') self.numLines = formatData.get('lines', type(self).defaultNumLines) self.sortKeyNum = formatData.get('sortkeynum', 0) self.sortKeyForward = formatData.get('sortkeyfwd', True) self.evalHtml = self.evalHtmlDefault if not self.fixEvalHtmlSetting: self.evalHtml = formatData.get('evalhtml', self.evalHtmlDefault) self.useFileInfo = False self.showInDialog = True self.setFormat(formatData.get('format', type(self).defaultFormat)) def formatData(self): """Return a dictionary of this field's format settings. """ formatData = {'fieldname': self.name, 'fieldtype': self.typeName} if self.format: formatData['format'] = self.format if self.prefix: formatData['prefix'] = self.prefix if self.suffix: formatData['suffix'] = self.suffix if self.initDefault: formatData['init'] = self.initDefault if self.numLines != self.defaultNumLines: formatData['lines'] = self.numLines if self.sortKeyNum > 0: formatData['sortkeynum'] = self.sortKeyNum if not self.sortKeyForward: formatData['sortkeyfwd'] = False if (not self.fixEvalHtmlSetting and self.evalHtml != self.evalHtmlDefault): formatData['evalhtml'] = self.evalHtml return formatData def setFormat(self, format): """Set the format string and initialize as required. Derived classes may raise a ValueError if the format is illegal. Arguments: format -- the new format string """ self.format = format def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): """Return formatted output text for this field in this node. Arguments: node -- the tree item storing the data oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix spotRef -- optional, used for ancestor field refs """ if self.useFileInfo and node.spotRefs: # get file info node if not already the file info node node = node.treeStructureRef().fileInfoNode storedText = node.data.get(self.name, '') if storedText: return self.formatOutput(storedText, oneLine, noHtml, formatHtml) return '' def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ prefix = self.prefix suffix = self.suffix if oneLine: storedText = _lineBreakRegEx.split(storedText, 1)[0] if noHtml: storedText = removeMarkup(storedText) if formatHtml: prefix = removeMarkup(prefix) suffix = removeMarkup(suffix) if not formatHtml: prefix = saxutils.escape(prefix) suffix = saxutils.escape(suffix) return '{0}{1}{2}'.format(prefix, storedText, suffix) def editorText(self, node): """Return text formatted for use in the data editor. The function for default text just returns the stored text. Overloads may raise a ValueError if the data does not match the format. Arguments: node -- the tree item storing the data """ storedText = node.data.get(self.name, '') return self.formatEditorText(storedText) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. The function for default text just returns the stored text. Overloads may raise a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ return storedText def storedText(self, editorText): """Return new text to be stored based on text from the data editor. The function for default text field just returns the editor text. Overloads may raise a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ return editorText def storedTextFromTitle(self, titleText): """Return new text to be stored based on title text edits. Overloads may raise a ValueError if the data does not match the format. Arguments: titleText -- the new title text """ return self.storedText(saxutils.escape(titleText)) def getInitDefault(self): """Return the initial stored value for newly created nodes. """ return self.initDefault def setInitDefault(self, editorText): """Set the default initial value from editor text. The function for default text field just returns the stored text. Arguments: editorText -- the new text entered into the editor """ self.initDefault = self.storedText(editorText) def getEditorInitDefault(self): """Return initial value in editor format. """ value = '' if self.supportsInitDefault: try: value = self.formatEditorText(self.initDefault) except ValueError: pass return value def initDefaultChoices(self): """Return a list of choices for setting the init default. """ return [] def mathValue(self, node, zeroBlanks=True, noMarkup=True): """Return a value to be used in math field equations. Return None if blank and not zeroBlanks. Arguments: node -- the tree item storing the data zeroBlanks -- accept blank field values if True noMarkup -- if true, remove html markup """ storedText = node.data.get(self.name, '') if storedText and noMarkup: storedText = removeMarkup(storedText) return storedText if storedText or zeroBlanks else None def compareValue(self, node): """Return a value for comparison to other nodes and for sorting. Returns lowercase text for text fields or numbers for non-text fields. Arguments: node -- the tree item storing the data """ storedText = node.data.get(self.name, '') return self.adjustedCompareValue(storedText) def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Text version removes any markup and goes to lower case. Arguments: value -- the comparison value to adjust """ value = removeMarkup(value) return value.lower() def sortKey(self, node): """Return a tuple with field type and comparison value for sorting. Allows different types to be sorted. Arguments: node -- the tree item storing the data """ return (self.sortTypeStr, self.compareValue(node)) def changeType(self, newType): """Change this field's type to newType with a default format. Arguments: newType -- the new type name, excluding "Field" """ self.__class__ = globals()[newType + 'Field'] self.setFormat(self.defaultFormat) if self.fixEvalHtmlSetting: self.evalHtml = self.evalHtmlDefault def sepName(self): """Return the name enclosed with {* *} separators """ if self.useFileInfo: return '{{*!{0}*}}'.format(self.name) return '{{*{0}*}}'.format(self.name) def getFormatHelpMenuList(self): """Return the list of descriptions and keys for the format help menu. """ return self.formatHelpMenuList class HtmlTextField(TextField): """Class to handle an HTML text field format type Stores options and format strings for an HTML text field type. Does not use the rich text editor. Provides methods to return formatted data. """ typeName = 'HtmlText' showRichTextInCell = False evalHtmlDefault = True editorClassName = 'HtmlTextEditor' def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ super().__init__(name, formatData) def storedTextFromTitle(self, titleText): """Return new text to be stored based on title text edits. Overloads may raise a ValueError if the data does not match the format. Arguments: titleText -- the new title text """ return self.storedText(titleText) class OneLineTextField(TextField): """Class to handle a single-line rich-text field format type. Stores options and format strings for a text field type. Provides methods to return formatted data. """ typeName = 'OneLineText' editorClassName = 'OneLineTextEditor' def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ super().__init__(name, formatData) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ text = _lineBreakRegEx.split(storedText, 1)[0] return super().formatOutput(text, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ return _lineBreakRegEx.split(storedText, 1)[0] class SpacedTextField(TextField): """Class to handle a preformatted text field format type. Stores options and format strings for a spaced text field type. Uses
     tags to preserve spacing.
        Does not use the rich text editor.
        Provides methods to return formatted data.
        """
        typeName = 'SpacedText'
        showRichTextInCell = False
        editorClassName = 'PlainTextEditor'
        def __init__(self, name, formatData=None):
            """Initialize a field format type.
    
            Arguments:
                name -- the field name string
                formatData -- the dict that defines this field's format
            """
            super().__init__(name, formatData)
    
        def formatOutput(self, storedText, oneLine, noHtml, formatHtml):
            """Return formatted output text from stored text for this field.
    
            Arguments:
                storedText -- the source text to format
                oneLine -- if True, returns only first line of output (for titles)
                noHtml -- if True, removes all HTML markup (for titles, etc.)
                formatHtml -- if False, escapes HTML from prefix & suffix
            """
            if storedText:
                storedText = '
    {0}
    '.format(storedText) return super().formatOutput(storedText, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Arguments: storedText -- the source text to format """ return saxutils.unescape(storedText) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Arguments: editorText -- the new text entered into the editor """ return saxutils.escape(editorText) def storedTextFromTitle(self, titleText): """Return new text to be stored based on title text edits. Arguments: titleText -- the new title text """ return self.storedText(titleText) class NumberField(HtmlTextField): """Class to handle a general number field format type. Stores options and format strings for a number field type. Provides methods to return formatted data. """ typeName = 'Number' defaultFormat = '#.##' evalHtmlDefault = False editorClassName = 'LineEditor' sortTypeStr = '20_num' formatHelpMenuList = [(_('Optional Digit\t#'), '#'), (_('Required Digit\t0'), '0'), (_('Digit or Space (external)\t'), ' '), ('', ''), (_('Decimal Point\t.'), '.'), (_('Decimal Comma\t,'), ','), ('', ''), (_('Comma Separator\t\\,'), '\\,'), (_('Dot Separator\t\\.'), '\\.'), (_('Space Separator (internal)\t'), ' '), ('', ''), (_('Optional Sign\t-'), '-'), (_('Required Sign\t+'), '+'), ('', ''), (_('Exponent (capital)\tE'), 'E'), (_('Exponent (small)\te'), 'e')] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ super().__init__(name, formatData) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ try: text = gennumber.GenNumber(storedText).numStr(self.format) except ValueError: text = _errorStr return super().formatOutput(text, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if not storedText: return '' return gennumber.GenNumber(storedText).numStr(self.format) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ if not editorText: return '' return repr(gennumber.GenNumber().setFromStr(editorText, self.format)) def mathValue(self, node, zeroBlanks=True, noMarkup=True): """Return a numeric value to be used in math field equations. Return None if blank and not zeroBlanks, raise a ValueError if it isn't a number. Arguments: node -- the tree item storing the data zeroBlanks -- replace blank field values with zeros if True noMarkup -- not applicable to numbers """ storedText = node.data.get(self.name, '') if storedText: return gennumber.GenNumber(storedText).num return 0 if zeroBlanks else None def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Number version converts to a numeric value. Arguments: value -- the comparison value to adjust """ try: return gennumber.GenNumber(value).num except ValueError: return 0 class MathField(HtmlTextField): """Class to handle a math calculation field type. Stores options and format strings for a math field type. Provides methods to return formatted data. """ typeName = 'Math' defaultFormat = '#.##' evalHtmlDefault = False fixEvalHtmlSetting = False editorClassName = 'ReadOnlyEditor' def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the attributes that define this field's format """ super().__init__(name, formatData) self.equation = None self.resultType = MathResult[formatData.get('resulttype', 'number')] equationText = formatData.get('eqn', '').strip() if equationText: self.equation = matheval.MathEquation(equationText) try: self.equation.validate() except ValueError: self.equation = None def formatData(self): """Return a dictionary of this field's attributes. Add the math equation to the standard XML output. """ formatData = super().formatData() if self.equation: formatData['eqn'] = self.equation.equationText() if self.resultType != MathResult.number: formatData['resulttype'] = self.resultType.name return formatData def setFormat(self, format): """Set the format string and initialize as required. Arguments: format -- the new format string """ if not hasattr(self, 'equation'): self.equation = None self.resultType = MathResult.number super().setFormat(format) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ text = storedText try: if self.resultType == MathResult.number: text = gennumber.GenNumber(text).numStr(self.format) elif self.resultType == MathResult.date: date = datetime.datetime.strptime(text, DateField.isoFormat).date() text = date.strftime(adjOutDateFormat(self.format)) elif self.resultType == MathResult.time: time = datetime.datetime.strptime(text, TimeField.isoFormat).time() text = time.strftime(adjOutDateFormat(self.format)) elif self.resultType == MathResult.boolean: text = genboolean.GenBoolean(text).boolStr(self.format) except ValueError: text = _errorStr return super().formatOutput(text, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if not storedText: return '' if self.resultType == MathResult.number: return gennumber.GenNumber(storedText).numStr(self.format) if self.resultType == MathResult.date: date = datetime.datetime.strptime(storedText, DateField.isoFormat).date() editorFormat = adjOutDateFormat(globalref. genOptions['EditDateFormat']) return date.strftime(editorFormat) if self.resultType == MathResult.time: time = datetime.datetime.strptime(storedText, TimeField.isoFormat).time() editorFormat = adjOutDateFormat(globalref. genOptions['EditTimeFormat']) return time.strftime(editorFormat) if self.resultType == MathResult.boolean: return genboolean.GenBoolean(storedText).boolStr(self.format) if storedText == _errorStr: raise ValueError return storedText def equationText(self): """Return the current equation text. """ if self.equation: return self.equation.equationText() return '' def equationValue(self, node): """Return a text value from the result of the equation. Returns the '#####' error string for illegal math operations. Arguments: node -- the tree item with this equation """ if self.equation: zeroValue = _mathResultBlank[self.resultType] try: num = self.equation.equationValue(node, self.resultType, zeroValue, not self.evalHtml) except ValueError: return _errorStr if num == None: return '' if self.resultType == MathResult.date: date = DateField.refDate + datetime.timedelta(days=num) return date.strftime(DateField.isoFormat) if self.resultType == MathResult.time: dateTime = datetime.datetime.combine(DateField.refDate, TimeField.refTime) dateTime = dateTime + datetime.timedelta(seconds=num) time = dateTime.time() return time.strftime(TimeField.isoFormat) text = str(num) if not self.evalHtml: text = saxutils.escape(text) return text return '' def resultClass(self): """Return the result type's field class. """ return globals()[self.resultType.name.capitalize() + 'Field'] def changeResultType(self, resultType): """Change the result type and reset the output format. Arguments: resultType -- the new result type """ if resultType != self.resultType: self.resultType = resultType self.setFormat(self.resultClass().defaultFormat) def mathValue(self, node, zeroBlanks=True, noMarkup=True): """Return a numeric value to be used in math field equations. Return None if blank and not zeroBlanks, raise a ValueError if it isn't valid. Arguments: node -- the tree item storing the data zeroBlanks -- replace blank field values with zeros if True noMarkup -- if true, remove html markup """ storedText = node.data.get(self.name, '') if storedText: if self.resultType == MathResult.number: return gennumber.GenNumber(storedText).num if self.resultType == MathResult.date: date = datetime.datetime.strptime(storedText, DateField.isoFormat).date() return (date - DateField.refDate).days if self.resultType == MathResult.time: time = datetime.datetime.strptime(storedText, TimeField.isoFormat).time() return (time - TimeField.refTime).seconds if self.resultType == MathResult.boolean: return genboolean.GenBoolean(storedText).value if noMarkup: storedText = removeMarkup(storedText) return storedText return _mathResultBlank[self.resultType] if zeroBlanks else None def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Number version converts to a numeric value. Arguments: value -- the comparison value to adjust """ try: if self.resultType == MathResult.number: return gennumber.GenNumber(value).num if self.resultType == MathResult.date: date = datetime.datetime.strptime(value, DateField.isoFormat).date() return date.strftime(DateField.isoFormat) if self.resultType == MathResult.time: time = datetime.datetime.strptime(value, TimeField.isoFormat).time() return time.strftime(TimeField.isoFormat) if self.resultType == MathResult.boolean: return genboolean.GenBoolean(value).value return value.lower() except ValueError: return 0 def sortKey(self, node): """Return a tuple with field type and comparison value for sorting. Allows different types to be sorted. Arguments: node -- the tree item storing the data """ return (self.resultClass().sortTypeStr, self.compareValue(node)) def getFormatHelpMenuList(self): """Return the list of descriptions and keys for the format help menu. """ return self.resultClass().formatHelpMenuList class NumberingField(HtmlTextField): """Class to handle formats for hierarchical node numbering. Stores options and format strings for a node numbering field type. Provides methods to return formatted node numbers. """ typeName = 'Numbering' defaultFormat = '1..' evalHtmlDefault = False editorClassName = 'LineEditor' sortTypeStr = '10_numbering' formatHelpMenuList = [(_('Number\t1'), '1'), (_('Capital Letter\tA'), 'A'), (_('Small Letter\ta'), 'a'), (_('Capital Roman Numeral\tI'), 'I'), (_('Small Roman Numeral\ti'), 'i'), ('', ''), (_('Level Separator\t/'), '/'), (_('Section Separator\t.'), '.'), ('', ''), (_('"/" Character\t//'), '//'), (_('"." Character\t..'), '..'), ('', ''), (_('Outline Example\tI../A../1../a)/i)'), 'I../A../1../a)/i)'), (_('Section Example\t1.1.1.1'), '1.1.1.1')] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the attributes that define this field's format """ self.numFormat = None super().__init__(name, formatData) def setFormat(self, format): """Set the format string and initialize as required. Arguments: format -- the new format string """ self.numFormat = numbering.NumberingGroup(format) super().setFormat(format) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ try: text = self.numFormat.numString(storedText) except ValueError: text = _errorStr return super().formatOutput(text, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if storedText: checkData = [int(num) for num in storedText.split('.')] return storedText def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ if editorText: checkData = [int(num) for num in editorText.split('.')] return editorText def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Number version converts to a numeric value. Arguments: value -- the comparison value to adjust """ if value: try: return [int(num) for num in value.split('.')] except ValueError: pass return [0] class DateField(HtmlTextField): """Class to handle a general date field format type. Stores options and format strings for a date field type. Provides methods to return formatted data. """ typeName = 'Date' defaultFormat = '%B %-d, %Y' isoFormat = '%Y-%m-%d' evalHtmlDefault = False fixEvalHtmlSetting = False editorClassName = 'DateEditor' refDate = datetime.date(1970, 1, 1) sortTypeStr = '40_date' formatHelpMenuList = [(_('Day (1 or 2 digits)\t%-d'), '%-d'), (_('Day (2 digits)\t%d'), '%d'), ('', ''), (_('Weekday Abbreviation\t%a'), '%a'), (_('Weekday Name\t%A'), '%A'), ('', ''), (_('Month (1 or 2 digits)\t%-m'), '%-m'), (_('Month (2 digits)\t%m'), '%m'), (_('Month Abbreviation\t%b'), '%b'), (_('Month Name\t%B'), '%B'), ('', ''), (_('Year (2 digits)\t%y'), '%y'), (_('Year (4 digits)\t%Y'), '%Y'), ('', ''), (_('Week Number (0 to 53)\t%-U'), '%-U'), (_('Day of year (1 to 366)\t%-j'), '%-j')] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ super().__init__(name, formatData) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ try: date = datetime.datetime.strptime(storedText, DateField.isoFormat).date() text = date.strftime(adjOutDateFormat(self.format)) except ValueError: text = _errorStr if not self.evalHtml: text = saxutils.escape(text) return super().formatOutput(text, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if not storedText: return '' date = datetime.datetime.strptime(storedText, DateField.isoFormat).date() editorFormat = adjOutDateFormat(globalref.genOptions['EditDateFormat']) return date.strftime(editorFormat) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Two digit years are interpretted as 1950-2049. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ editorText = _multipleSpaceRegEx.sub(' ', editorText.strip()) if not editorText: return '' editorFormat = adjInDateFormat(globalref.genOptions['EditDateFormat']) try: date = datetime.datetime.strptime(editorText, editorFormat).date() except ValueError: # allow use of a 4-digit year to fix invalid dates fullYearFormat = editorFormat.replace('%y', '%Y') if fullYearFormat != editorFormat: date = datetime.datetime.strptime(editorText, fullYearFormat).date() else: raise return date.strftime(DateField.isoFormat) def getInitDefault(self): """Return the initial stored value for newly created nodes. """ if self.initDefault == _dateStampString: date = datetime.date.today() return date.strftime(DateField.isoFormat) return super().getInitDefault() def setInitDefault(self, editorText): """Set the default initial value from editor text. The function for default text field just returns the stored text. Arguments: editorText -- the new text entered into the editor """ if editorText == _dateStampString: self.initDefault = _dateStampString else: super().setInitDefault(editorText) def getEditorInitDefault(self): """Return initial value in editor format. """ if self.initDefault == _dateStampString: return _dateStampString return super().getEditorInitDefault() def initDefaultChoices(self): """Return a list of choices for setting the init default. """ return [_dateStampString] def mathValue(self, node, zeroBlanks=True, noMarkup=True): """Return a numeric value to be used in math field equations. Return None if blank and not zeroBlanks, raise a ValueError if it isn't a valid date. Arguments: node -- the tree item storing the data zeroBlanks -- replace blank field values with zeros if True """ storedText = node.data.get(self.name, '') if storedText: date = datetime.datetime.strptime(storedText, DateField.isoFormat).date() return (date - DateField.refDate).days return 0 if zeroBlanks else None def compareValue(self, node): """Return a value for comparison to other nodes and for sorting. Returns lowercase text for text fields or numbers for non-text fields. Date field uses ISO date format (YYY-MM-DD). Arguments: node -- the tree item storing the data """ return node.data.get(self.name, '') def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Date version converts to an ISO date format (YYYY-MM-DD). Arguments: value -- the comparison value to adjust """ value = _multipleSpaceRegEx.sub(' ', value.strip()) if not value: return '' if value == _dateStampString: date = datetime.date.today() return date.strftime(DateField.isoFormat) try: return self.storedText(value) except ValueError: return value class TimeField(HtmlTextField): """Class to handle a general time field format type Stores options and format strings for a time field type. Provides methods to return formatted data. """ typeName = 'Time' defaultFormat = '%-I:%M:%S %p' isoFormat = '%H:%M:%S.%f' evalHtmlDefault = False fixEvalHtmlSetting = False editorClassName = 'TimeEditor' numChoiceColumns = 2 autoAddChoices = False refTime = datetime.time() sortTypeStr = '50_time' formatHelpMenuList = [(_('Hour (0-23, 1 or 2 digits)\t%-H'), '%-H'), (_('Hour (00-23, 2 digits)\t%H'), '%H'), (_('Hour (1-12, 1 or 2 digits)\t%-I'), '%-I'), (_('Hour (01-12, 2 digits)\t%I'), '%I'), ('', ''), (_('Minute (1 or 2 digits)\t%-M'), '%-M'), (_('Minute (2 digits)\t%M'), '%M'), ('', ''), (_('Second (1 or 2 digits)\t%-S'), '%-S'), (_('Second (2 digits)\t%S'), '%S'), ('', ''), (_('Microseconds (6 digits)\t%f'), '%f'), ('', ''), (_('AM/PM\t%p'), '%p')] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the attributes that define this field's format """ super().__init__(name, formatData) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ try: time = datetime.datetime.strptime(storedText, TimeField.isoFormat).time() outFormat = adjOutDateFormat(self.format) outFormat = adjTimeAmPm(outFormat, time) text = time.strftime(outFormat) except ValueError: text = _errorStr if not self.evalHtml: text = saxutils.escape(text) return super().formatOutput(text, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if not storedText: return '' time = datetime.datetime.strptime(storedText, TimeField.isoFormat).time() editorFormat = adjOutDateFormat(globalref.genOptions['EditTimeFormat']) editorFormat = adjTimeAmPm(editorFormat, time) return time.strftime(editorFormat) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ editorText = _multipleSpaceRegEx.sub(' ', editorText.strip()) if not editorText: return '' editorFormat = adjInDateFormat(globalref.genOptions['EditTimeFormat']) time = None try: time = datetime.datetime.strptime(editorText, editorFormat).time() except ValueError: noSecFormat = editorFormat.replace(':%S', '') noSecFormat = _multipleSpaceRegEx.sub(' ', noSecFormat.strip()) try: time = datetime.datetime.strptime(editorText, noSecFormat).time() except ValueError: for altFormat in (editorFormat, noSecFormat): noAmFormat = altFormat.replace('%p', '') noAmFormat = _multipleSpaceRegEx.sub(' ', noAmFormat.strip()) try: time = datetime.datetime.strptime(editorText, noAmFormat).time() break except ValueError: pass if not time: raise ValueError return time.strftime(TimeField.isoFormat) def annotatedComboChoices(self, editorText): """Return a list of (choice, annotation) tuples for the combo box. Arguments: editorText -- the text entered into the editor """ editorFormat = adjOutDateFormat(globalref.genOptions['EditTimeFormat']) choices = [(datetime.datetime.now().time().strftime(editorFormat), '({0})'.format(_timeStampString))] for hour in (6, 9, 12, 15, 18, 21, 0): choices.append((datetime.time(hour).strftime(editorFormat), '')) return choices def getInitDefault(self): """Return the initial stored value for newly created nodes. """ if self.initDefault == _timeStampString: time = datetime.datetime.now().time() return time.strftime(TimeField.isoFormat) return super().getInitDefault() def setInitDefault(self, editorText): """Set the default initial value from editor text. The function for default text field just returns the stored text. Arguments: editorText -- the new text entered into the editor """ if editorText == _timeStampString: self.initDefault = _timeStampString else: super().setInitDefault(editorText) def getEditorInitDefault(self): """Return initial value in editor format. """ if self.initDefault == _timeStampString: return _timeStampString return super().getEditorInitDefault() def initDefaultChoices(self): """Return a list of choices for setting the init default. """ return [_timeStampString] def mathValue(self, node, zeroBlanks=True, noMarkup=True): """Return a numeric value to be used in math field equations. Return None if blank and not zeroBlanks, raise a ValueError if it isn't a valid time. Arguments: node -- the tree item storing the data zeroBlanks -- replace blank field values with zeros if True """ storedText = node.data.get(self.name, '') if storedText: time = datetime.datetime.strptime(storedText, TimeField.isoFormat).time() dateTime = datetime.datetime.combine(DateField.refDate, time) refDateTime = datetime.datetime.combine(DateField.refDate, TimeField.refTime) return (dateTime - refDateTime).seconds return 0 if zeroBlanks else None def compareValue(self, node): """Return a value for comparison to other nodes and for sorting. Returns lowercase text for text fields or numbers for non-text fields. Time field uses HH:MM:SS format. Arguments: node -- the tree item storing the data """ return node.data.get(self.name, '') def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Time version converts to HH:MM:SS format. Arguments: value -- the comparison value to adjust """ value = _multipleSpaceRegEx.sub(' ', value.strip()) if not value: return '' if value == _timeStampString: time = datetime.datetime.now().time() return time.strftime(TimeField.isoFormat) try: return self.storedText(value) except ValueError: return value class DateTimeField(HtmlTextField): """Class to handle a general date and time field format type. Stores options and format strings for a date and time field type. Provides methods to return formatted data. """ typeName = 'DateTime' defaultFormat = '%B %-d, %Y %-I:%M:%S %p' isoFormat = '%Y-%m-%d %H:%M:%S.%f' evalHtmlDefault = False fixEvalHtmlSetting = False editorClassName = 'DateTimeEditor' refDateTime = datetime.datetime(1970, 1, 1) sortTypeStr ='45_datetime' formatHelpMenuList = [(_('Day (1 or 2 digits)\t%-d'), '%-d'), (_('Day (2 digits)\t%d'), '%d'), ('', ''), (_('Weekday Abbreviation\t%a'), '%a'), (_('Weekday Name\t%A'), '%A'), ('', ''), (_('Month (1 or 2 digits)\t%-m'), '%-m'), (_('Month (2 digits)\t%m'), '%m'), (_('Month Abbreviation\t%b'), '%b'), (_('Month Name\t%B'), '%B'), ('', ''), (_('Year (2 digits)\t%y'), '%y'), (_('Year (4 digits)\t%Y'), '%Y'), ('', ''), (_('Week Number (0 to 53)\t%-U'), '%-U'), (_('Day of year (1 to 366)\t%-j'), '%-j'), (_('Hour (0-23, 1 or 2 digits)\t%-H'), '%-H'), (_('Hour (00-23, 2 digits)\t%H'), '%H'), (_('Hour (1-12, 1 or 2 digits)\t%-I'), '%-I'), (_('Hour (01-12, 2 digits)\t%I'), '%I'), ('', ''), (_('Minute (1 or 2 digits)\t%-M'), '%-M'), (_('Minute (2 digits)\t%M'), '%M'), ('', ''), (_('Second (1 or 2 digits)\t%-S'), '%-S'), (_('Second (2 digits)\t%S'), '%S'), ('', ''), (_('Microseconds (6 digits)\t%f'), '%f'), ('', ''), (_('AM/PM\t%p'), '%p')] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ super().__init__(name, formatData) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ try: dateTime = datetime.datetime.strptime(storedText, DateTimeField.isoFormat) outFormat = adjOutDateFormat(self.format) outFormat = adjTimeAmPm(outFormat, dateTime) text = dateTime.strftime(outFormat) except ValueError: text = _errorStr if not self.evalHtml: text = saxutils.escape(text) return super().formatOutput(text, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if not storedText: return '' dateTime = datetime.datetime.strptime(storedText, DateTimeField.isoFormat) editorFormat = '{0} {1}'.format(globalref.genOptions['EditDateFormat'], globalref.genOptions['EditTimeFormat']) editorFormat = adjOutDateFormat(editorFormat) editorFormat = adjTimeAmPm(editorFormat, dateTime) return dateTime.strftime(editorFormat) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Two digit years are interpretted as 1950-2049. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ editorText = _multipleSpaceRegEx.sub(' ', editorText.strip()) if not editorText: return '' editorFormat = '{0} {1}'.format(globalref.genOptions['EditDateFormat'], globalref.genOptions['EditTimeFormat']) editorFormat = adjInDateFormat(editorFormat) dateTime = None try: dateTime = datetime.datetime.strptime(editorText, editorFormat) except ValueError: noSecFormat = editorFormat.replace(':%S', '') noSecFormat = _multipleSpaceRegEx.sub(' ', noSecFormat.strip()) altFormats = [editorFormat, noSecFormat] for altFormat in altFormats[:]: noAmFormat = altFormat.replace('%p', '') noAmFormat = _multipleSpaceRegEx.sub(' ', noAmFormat.strip()) altFormats.append(noAmFormat) for altFormat in altFormats[:]: fullYearFormat = altFormat.replace('%y', '%Y') altFormats.append(fullYearFormat) for editorFormat in altFormats[1:]: try: dateTime = datetime.datetime.strptime(editorText, editorFormat) break except ValueError: pass if not dateTime: raise ValueError return dateTime.strftime(DateTimeField.isoFormat) def getInitDefault(self): """Return the initial stored value for newly created nodes. """ if self.initDefault == _timeStampString: dateTime = datetime.datetime.now() return dateTime.strftime(DateTimeField.isoFormat) return super().getInitDefault() def setInitDefault(self, editorText): """Set the default initial value from editor text. The function for default text field just returns the stored text. Arguments: editorText -- the new text entered into the editor """ if editorText == _timeStampString: self.initDefault = _timeStampString else: super().setInitDefault(editorText) def getEditorInitDefault(self): """Return initial value in editor format. """ if self.initDefault == _timeStampString: return _timeStampString return super().getEditorInitDefault() def initDefaultChoices(self): """Return a list of choices for setting the init default. """ return [_timeStampString] def mathValue(self, node, zeroBlanks=True, noMarkup=True): """Return a numeric value to be used in math field equations. Return None if blank and not zeroBlanks, raise a ValueError if it isn't a valid time. Arguments: node -- the tree item storing the data zeroBlanks -- replace blank field values with zeros if True """ storedText = node.data.get(self.name, '') if storedText: dateTime = datetime.datetime.strptime(storedText, DateTimeField.isoFormat) return (dateTime - DateTimeField.refDateTime).total_seconds() return 0 if zeroBlanks else None def compareValue(self, node): """Return a value for comparison to other nodes and for sorting. Returns lowercase text for text fields or numbers for non-text fields. DateTime field uses YYYY-MM-DD HH:MM:SS format. Arguments: node -- the tree item storing the data """ return node.data.get(self.name, '') def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Time version converts to HH:MM:SS format. Arguments: value -- the comparison value to adjust """ value = _multipleSpaceRegEx.sub(' ', value.strip()) if not value: return '' if value == _timeStampString: dateTime = datetime.datetime.now() return dateTime.strftime(DateTimeField.isoFormat) try: return self.storedText(value) except ValueError: return value class ChoiceField(HtmlTextField): """Class to handle a field with pre-defined, individual text choices. Stores options and format strings for a choice field type. Provides methods to return formatted data. """ typeName = 'Choice' editSep = '/' defaultFormat = '1/2/3/4' evalHtmlDefault = False fixEvalHtmlSetting = False editorClassName = 'ComboEditor' numChoiceColumns = 1 autoAddChoices = False formatHelpMenuList = [(_('Separator\t/'), '/'), ('', ''), (_('"/" Character\t//'), '//'), ('', ''), (_('Example\t1/2/3/4'), '1/2/3/4')] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ super().__init__(name, formatData) def setFormat(self, format): """Set the format string and initialize as required. Arguments: format -- the new format string """ super().setFormat(format) self.choiceList = self.splitText(self.format) if self.evalHtml: self.choices = set(self.choiceList) else: self.choices = set([saxutils.escape(choice) for choice in self.choiceList]) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ if storedText not in self.choices: storedText = _errorStr return super().formatOutput(storedText, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if storedText and storedText not in self.choices: raise ValueError if self.evalHtml: return storedText return saxutils.unescape(storedText) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ if not self.evalHtml: editorText = saxutils.escape(editorText) if not editorText or editorText in self.choices: return editorText raise ValueError def comboChoices(self): """Return a list of choices for the combo box. """ return self.choiceList def initDefaultChoices(self): """Return a list of choices for setting the init default. """ return self.choiceList def splitText(self, textStr): """Split textStr using editSep, return a list of strings. Double editSep's are not split (become single). Removes duplicates and empty strings. Arguments: textStr -- the text to split """ result = [] textStr = textStr.replace(self.editSep * 2, '\0') for text in textStr.split(self.editSep): text = text.strip().replace('\0', self.editSep) if text and text not in result: result.append(text) return result class AutoChoiceField(HtmlTextField): """Class to handle a field with automatically populated text choices. Stores options and possible entries for an auto-choice field type. Provides methods to return formatted data. """ typeName = 'AutoChoice' evalHtmlDefault = False fixEvalHtmlSetting = False editorClassName = 'ComboEditor' numChoiceColumns = 1 autoAddChoices = True def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the attributes that define this field's format """ super().__init__(name, formatData) self.choices = set() def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Arguments: storedText -- the source text to format """ if self.evalHtml: return storedText return saxutils.unescape(storedText) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Arguments: editorText -- the new text entered into the editor """ if self.evalHtml: return editorText return saxutils.escape(editorText) def comboChoices(self): """Return a list of choices for the combo box. """ if self.evalHtml: choices = self.choices else: choices = [saxutils.unescape(text) for text in self.choices] return sorted(choices, key=str.lower) def addChoice(self, text): """Add a new choice. Arguments: text -- the choice to be added """ if text: self.choices.add(text) def clearChoices(self): """Remove all current choices. """ self.choices = set() class CombinationField(ChoiceField): """Class to handle a field with multiple pre-defined text choices. Stores options and format strings for a combination field type. Provides methods to return formatted data. """ typeName = 'Combination' editorClassName = 'CombinationEditor' numChoiceColumns = 2 def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ super().__init__(name, formatData) def setFormat(self, format): """Set the format string and initialize as required. Arguments: format -- the new format string """ TextField.setFormat(self, format) if not self.evalHtml: format = saxutils.escape(format) self.choiceList = self.splitText(format) self.choices = set(self.choiceList) self.outputSep = '' def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): """Return formatted output text for this field in this node. Sets output separator prior to calling base class methods. Arguments: node -- the tree item storing the data oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix spotRef -- optional, used for ancestor field refs """ self.outputSep = node.formatRef.outputSeparator return super().outputText(node, oneLine, noHtml, formatHtml, spotRef) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ selections, valid = self.sortedSelections(storedText) if valid: result = self.outputSep.join(selections) else: result = _errorStr return TextField.formatOutput(self, result, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ selections = set(self.splitText(storedText)) if selections.issubset(self.choices): if self.evalHtml: return storedText return saxutils.unescape(storedText) raise ValueError def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ if not self.evalHtml: editorText = saxutils.escape(editorText) selections, valid = self.sortedSelections(editorText) if not valid: raise ValueError return self.joinText(selections) def comboChoices(self): """Return a list of choices for the combo box. """ if self.evalHtml: return self.choiceList return [saxutils.unescape(text) for text in self.choiceList] def comboActiveChoices(self, editorText): """Return a sorted list of choices currently in editorText. Arguments: editorText -- the text entered into the editor """ selections, valid = self.sortedSelections(saxutils.escape(editorText)) if self.evalHtml: return selections return [saxutils.unescape(text) for text in selections] def initDefaultChoices(self): """Return a list of choices for setting the init default. """ return [] def sortedSelections(self, inText): """Split inText using editSep and sort like format string. Return a tuple of resulting selection list and bool validity. Valid if all choices are in the format string. Arguments: inText -- the text to split and sequence """ selections = set(self.splitText(inText)) result = [text for text in self.choiceList if text in selections] return (result, len(selections) == len(result)) def joinText(self, textList): """Join the text list using editSep, return the string. Any editSep in text items become double. Arguments: textList -- the list of text items to join """ return self.editSep.join([text.replace(self.editSep, self.editSep * 2) for text in textList]) class AutoCombinationField(CombinationField): """Class for a field with multiple automatically populated text choices. Stores options and possible entries for an auto-choice field type. Provides methods to return formatted data. """ typeName = 'AutoCombination' autoAddChoices = True defaultFormat = '' formatHelpMenuList = [] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the attributes that define this field's format """ super().__init__(name, formatData) self.choices = set() self.outputSep = '' def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): """Return formatted output text for this field in this node. Sets output separator prior to calling base class methods. Arguments: node -- the tree item storing the data oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix spotRef -- optional, used for ancestor field refs """ self.outputSep = node.formatRef.outputSeparator return super().outputText(node, oneLine, noHtml, formatHtml, spotRef) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ result = self.outputSep.join(self.splitText(storedText)) return TextField.formatOutput(self, result, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Arguments: storedText -- the source text to format """ if self.evalHtml: return storedText return saxutils.unescape(storedText) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Also resets outputSep, to be defined at the next output. Arguments: editorText -- the new text entered into the editor """ self.outputSep = '' if not self.evalHtml: editorText = saxutils.escape(editorText) selections = sorted(self.splitText(editorText), key=str.lower) return self.joinText(selections) def comboChoices(self): """Return a list of choices for the combo box. """ if self.evalHtml: choices = self.choices else: choices = [saxutils.unescape(text) for text in self.choices] return sorted(choices, key=str.lower) def comboActiveChoices(self, editorText): """Return a sorted list of choices currently in editorText. Arguments: editorText -- the text entered into the editor """ selections, valid = self.sortedSelections(saxutils.escape(editorText)) if self.evalHtml: return selections return [saxutils.unescape(text) for text in selections] def sortedSelections(self, inText): """Split inText using editSep and sort like format string. Return a tuple of resulting selection list and bool validity. This version always returns valid. Arguments: inText -- the text to split and sequence """ selections = sorted(self.splitText(inText), key=str.lower) return (selections, True) def addChoice(self, text): """Add a new choice. Arguments: text -- the stored text combinations to be added """ for choice in self.splitText(text): self.choices.add(choice) def clearChoices(self): """Remove all current choices. """ self.choices = set() class BooleanField(ChoiceField): """Class to handle a general boolean field format type. Stores options and format strings for a boolean field type. Provides methods to return formatted data. """ typeName = 'Boolean' defaultFormat = _('yes/no') evalHtmlDefault = False fixEvalHtmlSetting = False sortTypeStr ='30_bool' formatHelpMenuList = [(_('true/false'), 'true/false'), (_('T/F'), 'T/F'), ('', ''), (_('yes/no'), 'yes/no'), (_('Y/N'), 'Y/N'), ('', ''), ('1/0', '1/0')] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ super().__init__(name, formatData) def setFormat(self, format): """Set the format string and initialize as required. Arguments: format -- the new format string """ HtmlTextField.setFormat(self, format) self.strippedFormat = removeMarkup(self.format) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ try: text = genboolean.GenBoolean(storedText).boolStr(self.format) except ValueError: text = _errorStr if not self.evalHtml: text = saxutils.escape(text) return HtmlTextField.formatOutput(self, text, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if not storedText: return '' boolFormat = self.strippedFormat if self.evalHtml else self.format return genboolean.GenBoolean(storedText).boolStr(boolFormat) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ if not editorText: return '' boolFormat = self.strippedFormat if self.evalHtml else self.format try: return repr(genboolean.GenBoolean().setFromStr(editorText, boolFormat)) except ValueError: return repr(genboolean.GenBoolean(editorText)) def comboChoices(self): """Return a list of choices for the combo box. """ if self.evalHtml: return self.splitText(self.strippedFormat) return self.splitText(self.format) def initDefaultChoices(self): """Return a list of choices for setting the init default. """ return self.comboChoices() def mathValue(self, node, zeroBlanks=True, noMarkup=True): """Return a value to be used in math field equations. Return None if blank and not zeroBlanks, raise a ValueError if it isn't a valid boolean. Arguments: node -- the tree item storing the data zeroBlanks -- replace blank field values with zeros if True """ storedText = node.data.get(self.name, '') if storedText: return genboolean.GenBoolean(storedText).value return False if zeroBlanks else None def compareValue(self, node): """Return a value for comparison to other nodes and for sorting. Returns lowercase text for text fields or numbers for non-text fields. Bool fields return True or False values. Arguments: node -- the tree item storing the data """ storedText = node.data.get(self.name, '') try: return genboolean.GenBoolean(storedText).value except ValueError: return False def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Bool version converts to a bool value. Arguments: value -- the comparison value to adjust """ try: return genboolean.GenBoolean().setFromStr(value, self.format).value except ValueError: try: return genboolean.GenBoolean(value).value except ValueError: return False class ExternalLinkField(HtmlTextField): """Class to handle a field containing various types of external HTML links. Protocol choices include http, https, file, mailto. Stores data as HTML tags, shows in editors as "protocol:address [name]". """ typeName = 'ExternalLink' evalHtmlDefault = False editorClassName = 'ExtLinkEditor' sortTypeStr ='60_link' def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the attributes that define this field's format """ super().__init__(name, formatData) def addressAndName(self, storedText): """Return the link title and the name from the given stored link. Raise ValueError if the stored text is not formatted as a link. Arguments: storedText -- the source text to format """ if not storedText: return ('', '') linkMatch = linkRegExp.search(storedText) if not linkMatch: raise ValueError address, name = linkMatch.groups() return (address, name) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ if noHtml: linkMatch = linkRegExp.search(storedText) if linkMatch: address, name = linkMatch.groups() storedText = name.strip() if not storedText: storedText = address.lstrip('#') return super().formatOutput(storedText, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if not storedText: return '' address, name = self.addressAndName(storedText) name = name.strip() if not name: name = urltools.shortName(address) return '{0} [{1}]'.format(address, name) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ if not editorText: return '' nameMatch = linkSeparateNameRegExp.match(editorText) if nameMatch: address, name = nameMatch.groups() else: raise ValueError return '{1}'.format(address.strip(), name.strip()) def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Link fields use link address. Arguments: value -- the comparison value to adjust """ if not value: return '' try: address, name = self.addressAndName(value) except ValueError: return value.lower() return address.lstrip('#').lower() class InternalLinkField(ExternalLinkField): """Class to handle a field containing internal links to nodes. Stores data as HTML local link tag, shows in editors as "id [name]". """ typeName = 'InternalLink' editorClassName = 'IntLinkEditor' supportsInitDefault = False def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the attributes that define this field's format """ super().__init__(name, formatData) def editorText(self, node): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Also raises a ValueError if the link is not a valid destination, with the editor text as the second argument to the exception. Arguments: node -- the tree item storing the data """ storedText = node.data.get(self.name, '') return self.formatEditorText(storedText, node.treeStructureRef()) def formatEditorText(self, storedText, treeStructRef): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Also raises a ValueError if the link is not a valid destination, with the editor text as the second argument to the exception. Arguments: storedText -- the source text to format treeStructRef -- ref to the tree structure to get the linked title """ if not storedText: return '' address, name = self.addressAndName(storedText) address = address.lstrip('#') targetNode = treeStructRef.nodeDict.get(address, None) linkTitle = targetNode.title() if targetNode else _errorStr name = name.strip() if not name and targetNode: name = linkTitle result = 'LinkTo: {0} [{1}]'.format(linkTitle, name) if linkTitle == _errorStr: raise ValueError('invalid address', result) return result def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Uses the "address [name]" format as input, not the final editor form. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new editor text in "address [name]" format """ if not editorText: return '' nameMatch = linkSeparateNameRegExp.match(editorText) if not nameMatch: raise ValueError address, name = nameMatch.groups() if not address: raise ValueError('invalid address', '') if not name: name = _errorStr result = '{1}'.format(address.strip(), name.strip()) if name == _errorStr: raise ValueError('invalid name', result) return result class PictureField(HtmlTextField): """Class to handle a field containing various types of external HTML links. Protocol choices include http, https, file, mailto. Stores data as HTML tags, shows in editors as "protocol:address [name]". """ typeName = 'Picture' evalHtmlDefault = False editorClassName = 'PictureLinkEditor' sortTypeStr ='60_link' def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the attributes that define this field's format """ super().__init__(name, formatData) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ if noHtml: linkMatch = _imageRegExp.search(storedText) if linkMatch: address = linkMatch.group(1) storedText = address.strip() return super().formatOutput(storedText, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if not storedText: return '' linkMatch = _imageRegExp.search(storedText) if not linkMatch: raise ValueError return linkMatch.group(1) def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ editorText = editorText.strip() if not editorText: return '' nameMatch = linkSeparateNameRegExp.match(editorText) if nameMatch: address, name = nameMatch.groups() else: address = editorText name = urltools.shortName(address) return ''.format(editorText) def adjustedCompareValue(self, value): """Return value adjusted like the compareValue for use in conditionals. Link fields use link address. Arguments: value -- the comparison value to adjust """ if not value: return '' linkMatch = _imageRegExp.search(value) if not linkMatch: return value.lower() return linkMatch.group(1).lower() class RegularExpressionField(HtmlTextField): """Class to handle a field format type controlled by a regular expression. Stores options and format strings for a number field type. Provides methods to return formatted data. """ typeName = 'RegularExpression' defaultFormat = '.*' evalHtmlDefault = False fixEvalHtmlSetting = False editorClassName = 'LineEditor' formatHelpMenuList = [(_('Any Character\t.'), '.'), (_('End of Text\t$'), '$'), ('', ''), (_('0 Or More Repetitions\t*'), '*'), (_('1 Or More Repetitions\t+'), '+'), (_('0 Or 1 Repetitions\t?'), '?'), ('', ''), (_('Set of Numbers\t[0-9]'), '[0-9]'), (_('Lower Case Letters\t[a-z]'), '[a-z]'), (_('Upper Case Letters\t[A-Z]'), '[A-Z]'), (_('Not a Number\t[^0-9]'), '[^0-9]'), ('', ''), (_('Or\t|'), '|'), (_('Escape a Special Character\t\\'), '\\')] def __init__(self, name, formatData=None): """Initialize a field format type. Arguments: name -- the field name string formatData -- the dict that defines this field's format """ super().__init__(name, formatData) def setFormat(self, format): """Set the format string and initialize as required. Raise a ValueError if the format is illegal. Arguments: format -- the new format string """ try: re.compile(format) except re.error: raise ValueError super().setFormat(format) def formatOutput(self, storedText, oneLine, noHtml, formatHtml): """Return formatted output text from stored text for this field. Arguments: storedText -- the source text to format oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix """ match = re.fullmatch(self.format, saxutils.unescape(storedText)) if not storedText or match: text = storedText else: text = _errorStr return super().formatOutput(text, oneLine, noHtml, formatHtml) def formatEditorText(self, storedText): """Return text formatted for use in the data editor. Raises a ValueError if the data does not match the format. Arguments: storedText -- the source text to format """ if not self.evalHtml: storedText = saxutils.unescape(storedText) match = re.fullmatch(self.format, storedText) if not storedText or match: return storedText raise ValueError def storedText(self, editorText): """Return new text to be stored based on text from the data editor. Raises a ValueError if the data does not match the format. Arguments: editorText -- the new text entered into the editor """ match = re.fullmatch(self.format, editorText) if not editorText or match: if self.evalHtml: return editorText return saxutils.escape(editorText) raise ValueError class AncestorLevelField(TextField): """Placeholder format for ref. to ancestor fields at specific levels. """ typeName = 'AncestorLevel' def __init__(self, name, ancestorLevel=1): """Initialize a field format placeholder type. Arguments: name -- the field name string ancestorLevel -- the number of generations to go back """ super().__init__(name, {}) self.ancestorLevel = ancestorLevel def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): """Return formatted output text for this field in this node. Finds the appropriate ancestor node to get the field text. Arguments: node -- the tree node to start from oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix spotRef -- optional, used for ancestor field refs """ if not spotRef: spotRef = node.spotByNumber(0) for num in range(self.ancestorLevel): spotRef = spotRef.parentSpot if not spotRef: return '' try: field = spotRef.nodeRef.formatRef.fieldDict[self.name] except (AttributeError, KeyError): return '' return field.outputText(spotRef.nodeRef, oneLine, noHtml, formatHtml, spotRef) def sepName(self): """Return the name enclosed with {* *} separators """ return '{{*{0}{1}*}}'.format(self.ancestorLevel * '*', self.name) class AnyAncestorField(TextField): """Placeholder format for ref. to matching ancestor fields at any level. """ typeName = 'AnyAncestor' def __init__(self, name): """Initialize a field format placeholder type. Arguments: name -- the field name string """ super().__init__(name, {}) def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): """Return formatted output text for this field in this node. Finds the appropriate ancestor node to get the field text. Arguments: node -- the tree node to start from oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix spotRef -- optional, used for ancestor field refs """ if not spotRef: spotRef = node.spotByNumber(0) while spotRef.parentSpot: spotRef = spotRef.parentSpot try: field = spotRef.nodeRef.formatRef.fieldDict[self.name] except (AttributeError, KeyError): pass else: return field.outputText(spotRef.nodeRef, oneLine, noHtml, formatHtml, spotRef) return '' def sepName(self): """Return the name enclosed with {* *} separators """ return '{{*?{0}*}}'.format(self.name) class ChildListField(TextField): """Placeholder format for ref. to matching ancestor fields at any level. """ typeName = 'ChildList' def __init__(self, name): """Initialize a field format placeholder type. Arguments: name -- the field name string """ super().__init__(name, {}) def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): """Return formatted output text for this field in this node. Returns a joined list of matching child field data. Arguments: node -- the tree node to start from oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix spotRef -- optional, used for ancestor field refs """ result = [] for child in node.childList: try: field = child.formatRef.fieldDict[self.name] except KeyError: pass else: result.append(field.outputText(child, oneLine, noHtml, formatHtml, spotRef)) outputSep = node.formatRef.outputSeparator return outputSep.join(result) def sepName(self): """Return the name enclosed with {* *} separators """ return '{{*&{0}*}}'.format(self.name) class DescendantCountField(TextField): """Placeholder format for count of descendants at a given level. """ typeName = 'DescendantCount' def __init__(self, name, descendantLevel=1): """Initialize a field format placeholder type. Arguments: name -- the field name string descendantLevel -- the level to descend to """ super().__init__(name, {}) self.descendantLevel = descendantLevel def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): """Return formatted output text for this field in this node. Returns a count of descendants at the approriate level. Arguments: node -- the tree node to start from oneLine -- if True, returns only first line of output (for titles) noHtml -- if True, removes all HTML markup (for titles, etc.) formatHtml -- if False, escapes HTML from prefix & suffix spotRef -- optional, used for ancestor field refs """ newNodes = [node] for i in range(self.descendantLevel): prevNodes = newNodes newNodes = [] for child in prevNodes: newNodes.extend(child.childList) return repr(len(newNodes)) def sepName(self): """Return the name enclosed with {* *} separators """ return '{{*#{0}*}}'.format(self.name) #### Utility Functions #### def removeMarkup(text): """Return text with all HTML Markup removed and entities unescaped. Any
    tags are replaced with newlines. """ text = _lineBreakRegEx.sub('\n', text) text = _stripTagRe.sub('', text) return saxutils.unescape(text) def adjOutDateFormat(dateFormat): """Replace Linux lead zero removal with Windows version in date formats. Arguments: dateFormat -- the format to modify """ if sys.platform.startswith('win'): dateFormat = dateFormat.replace('%-', '%#') return dateFormat def adjInDateFormat(dateFormat): """Remove lead zero formatting in date formats for reading dates. Arguments: dateFormat -- the format to modify """ return dateFormat.replace('%-', '%') def adjTimeAmPm(timeFormat, time): """Add AM/PM to timeFormat if in format and locale skips it. Arguments: timeFormat -- the format to modify time -- the datetime object to check for AM/PM """ if '%p' in timeFormat and time.strftime('%I (%p)').endswith('()'): amPm = 'AM' if time.hour < 12 else 'PM' timeFormat = re.sub(r'(?]*href="#(.*?)"[^>]*>.*?', re.I | re.S) _genLinkRe = re.compile(r']*href="(.*?)"[^>]*>.*?', re.I | re.S) _imgLinkRe = re.compile(r']*src="(.*?)"[^>]*>.*?', re.I | re.S) _idReplaceCharsRe = re.compile(r'[^a-zA-Z0-9_-]+') class ExportControl: """Control to do file exports for tree branches and nodes. """ def __init__(self, structure, selectionModel, defaultPathObj, printData): """Initialize export control object. Arguments: structure -- the tree structure ref for exporting the entire tree selectionModel -- the selection model for partial exports defaultPathObj -- path object to use as file dialog default printData -- a ref to print data for old treeline exports """ self.structure = structure self.selectedSpots = selectionModel.selectedSpots() self.selectedNodes = selectionModel.selectedNodes() self.defaultPathObj = defaultPathObj self.printData = printData def interactiveExport(self): """Prompt the user for types, options, filename & proceed with export. Return True if export is successful. """ exportMethods = {'htmlSingle': self.exportHtmlSingle, 'htmlNavSingle': self.exportHtmlNavSingle, 'htmlPages': self.exportHtmlPages, 'htmlTables': self.exportHtmlTables, 'htmlLiveLink': self.exportHtmlLiveLink, 'htmlLiveSingle': self.exportHtmlLiveSingle, 'textTitles': self.exportTextTitles, 'textPlain': self.exportTextPlain, 'textTableMultiCsv': self.exportTextTableMultiCsv, 'textTableCsv': self.exportTextTableCsv, 'textTableTab': self.exportTextTableTab, 'oldTreeLine': self.exportOldTreeLine, 'treeLineSubtree': self.exportSubtree, 'xmlGeneric': self.exportXmlGeneric, 'odfText': self.exportOdfText, 'bookmarksHtml': self.exportBookmarksHtml, 'bookmarksXbel': self.exportBookmarksXbel} exportDialog = ExportDialog(len(self.selectedNodes), QApplication.activeWindow()) if exportDialog.exec_() == QDialog.Accepted: result = exportMethods[ExportDialog.currentSubtype]() QApplication.restoreOverrideCursor() return result return False def getFileName(self, dialogTitle, defaultExt='txt'): """Prompt the user for a filename and return a path object. Arguments: dialogTitle -- the title for use on the dialog window defaultExt -- the default file extension from globalref """ filters = ';;'.join((globalref.fileFilters[defaultExt], globalref.fileFilters['all'])) defaultExt = defaultExt[:4] if self.defaultPathObj.is_file(): self.defaultPathObj = self.defaultPathObj.with_suffix('.' + defaultExt) filePath, selectFilter = QFileDialog.getSaveFileName(QApplication. activeWindow(), dialogTitle, str(self.defaultPathObj), filters) if filePath: pathObj = pathlib.Path(filePath) if not pathObj.suffix: pathObj = pathObj.with_suffix('.' + defaultExt) return pathObj return None def exportHtmlSingle(self, pathObj=None): """Export to a single web page, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export HTML'), 'html') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.entireTree: self.selectedSpots = self.structure.rootSpots() outputGroup = treeoutput.OutputGroup(self.selectedSpots, ExportDialog.includeRoot, ExportDialog.exportWhat != ExportDialog.selectNode, ExportDialog.openOnly) outputGroup.addAnchors() if outputGroup.hasPrefixes(): outputGroup.combineAllSiblings() outputGroup.addBlanksBetween() outputGroup.addIndents() outGroups = outputGroup.splitColumns(ExportDialog.numColumns) htmlTitle = pathObj.stem indent = globalref.genOptions['IndentOffset'] lines = ['', '', '', '', '{0}'.format(htmlTitle), '', '', ''] if ExportDialog.addHeader: headerText = (globalref.mainControl.activeControl.printData. formatHeaderFooter(True)) if headerText: lines.append(headerText) lines.extend(['', '', '
    ']) lines.extend(outGroups[0].getLines()) for group in outGroups[1:]: lines.append('') lines.extend(group.getLines()) lines.extend(['
    ']) if ExportDialog.addHeader: footerText = (globalref.mainControl.activeControl.printData. formatHeaderFooter(False)) if footerText: lines.append(footerText) lines.extend(['', '']) with pathObj.open('w', encoding='utf-8') as f: f.writelines([(line + '\n') for line in lines]) return True def exportHtmlNavSingle(self, pathObj=None): """Export single web page with a navigation pane, ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export HTML'), 'html') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.entireTree: self.selectedSpots = self.structure.rootSpots() outputGroup = treeoutput.OutputGroup(self.selectedSpots, ExportDialog.includeRoot, True, ExportDialog.openOnly) outputGroup.addAnchors(ExportDialog.navPaneLevels) if outputGroup.hasPrefixes(): outputGroup.combineAllSiblings() outputGroup.addBlanksBetween() outputGroup.addIndents() htmlTitle = pathObj.stem indent = globalref.genOptions['IndentOffset'] lines = ['', '', '', '', '{0}'.format(htmlTitle), '', '', '', '') level -= 1 lines.extend(['
    ', '
    ']) if ExportDialog.addHeader: headerText = (globalref.mainControl.activeControl.printData. formatHeaderFooter(True)) if headerText: lines.append(headerText) lines.extend(outputGroup.getLines()) if ExportDialog.addHeader: footerText = (globalref.mainControl.activeControl.printData. formatHeaderFooter(False)) if footerText: lines.append(footerText) lines.extend(['
    ', '', '']) with pathObj.open('w', encoding='utf-8') as f: f.writelines([(line + '\n') for line in lines]) return True def exportHtmlPages(self, pathObj=None): """Export multiple web pages with navigation, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: path = QFileDialog.getExistingDirectory(QApplication. activeWindow(), _('TreeLine - Export HTML'), str(self.defaultPathObj)) if not path: return False pathObj = pathlib.Path(path) QApplication.setOverrideCursor(Qt.WaitCursor) oldDir = os.getcwd() os.chdir(str(pathObj)) indent = globalref.genOptions['IndentOffset'] cssLines = ['#sidebar {', ' width: 16em;', ' float: left;', ' border-right: 1px solid black;', '}', '#sidebar div {{margin-left: {0}em;}}'.format(indent), '#content {', ' margin-left: 16em;', ' border-left: 1px solid black;', ' padding-left: 6px;', '}'] with open('default.css', 'w', encoding='utf-8') as f: f.writelines([(line + '\n') for line in cssLines]) if ExportDialog.exportWhat != ExportDialog.entireTree: self.structure = treestructure.TreeStructure(topNodes=self. selectedNodes, addSpots=False) if len(self.structure.childList) > 1: self.structure = treestructure.TreeStructure(topNodes=self. structure.childList, addSpots=False) rootType = nodeformat.NodeFormat(treeformats.defaultTypeName, self.structure.treeFormats, addDefaultField=True) self.structure.treeFormats.addTypeIfMissing(rootType) root = treenode.TreeNode(self.structure. treeFormats[treeformats.defaultTypeName]) root.setTitle(treestructure.defaultRootTitle) self.structure.addNodeDictRef(root) root.childList = self.structure.childList self.structure.childList = [root] pathDict = {} _setHtmlDirectories(self.structure.childList[0], pathDict, pathObj, set()) _writeHtmlPage(self.structure.childList[0], None, None, pathDict) os.chdir(oldDir) return True def exportHtmlTables(self, pathObj=None): """Export to multiple web page tables, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: path = QFileDialog.getExistingDirectory(QApplication. activeWindow(), _('TreeLine - Export HTML'), str(self.defaultPathObj)) if not path: return False pathObj = pathlib.Path(path) QApplication.setOverrideCursor(Qt.WaitCursor) oldDir = os.getcwd() os.chdir(str(pathObj)) if ExportDialog.exportWhat != ExportDialog.entireTree: self.structure = treestructure.TreeStructure(topNodes=self. selectedNodes, addSpots=False) if len(self.structure.childList) > 1: self.structure = treestructure.TreeStructure(topNodes=self. structure.childList, addSpots=False) rootType = nodeformat.NodeFormat(treeformats.defaultTypeName, self.structure.treeFormats, addDefaultField=True) self.structure.treeFormats.addTypeIfMissing(rootType) root = treenode.TreeNode(self.structure. treeFormats[treeformats.defaultTypeName]) name = self.defaultPathObj.stem if not name: name = treestructure.defaultRootTitle root.setTitle(name) self.structure.addNodeDictRef(root) root.childList = self.structure.childList self.structure.childList = [root] pathDict = {} _setHtmlDirectories(self.structure.childList[0], pathDict, pathObj, set(), False) _writeHtmlTable(self.structure.childList[0], None, pathDict) os.chdir(oldDir) return True def exportHtmlLiveLink(self, pathObj=None): """Export a live tree view, linked back to the source file. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: path = QFileDialog.getExistingDirectory(QApplication. activeWindow(), _('TreeLine - Export HTML'), str(self.defaultPathObj)) if not path: return False pathObj = pathlib.Path(path) QApplication.setOverrideCursor(Qt.WaitCursor) control = globalref.mainControl prefPath = templatePath + '/exports' if templatePath else '' htmlPath = control.findResourceFile('live_tree_export.html', 'templates/exports', prefPath) jsPath = control.findResourceFile('live_tree_export.js', 'templates/exports', prefPath) cssPath = control.findResourceFile('live_tree_export.css', 'templates/exports', prefPath) if not htmlPath or not jsPath or not cssPath: QApplication.restoreOverrideCursor() QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - export template files not found.\n' 'Check your TreeLine installation.')) return False refPath = globalref.mainControl.activeControl.filePathObj if not refPath: QApplication.restoreOverrideCursor() QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - cannot link to unsaved TreeLine ' 'file.\nSave the file and retry.')) return False try: refPath = pathlib.Path(os.path.relpath(str(refPath), str(pathObj))) except ValueError: QApplication.restoreOverrideCursor() msg = _('Warning - no relative path from "{0}" to "{1}".\n' 'Continue with absolute path?').format(pathObj.as_posix(), refPath.as_posix()) ans = QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if ans == QMessageBox.No: return False QApplication.setOverrideCursor(Qt.WaitCursor) fileStem = refPath.stem outPath = pathObj / (fileStem + '.html') with htmlPath.open(encoding='utf-8') as fileIn: with outPath.open('w', encoding='utf-8') as fileOut: for line in fileIn: if '' in line: line = re.sub(r'<title>.*', '{0}'.format(fileStem), line) elif 'dataFilePath' in line: line = line.replace('""', '"{0}"'. format(refPath.parent.as_posix())) elif 'dataFileName' in line: line = line.replace('""', '"{0}"'.format(refPath.name)) fileOut.write(line) shutil.copy(str(jsPath), str(pathObj)) shutil.copy(str(cssPath), str(pathObj)) return True def exportHtmlLiveSingle(self, pathObj=None): """Export a live tree view to a single file (embedded data). Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export HTML'), 'html') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) control = globalref.mainControl prefPath = templatePath + '/exports' if templatePath else '' htmlPath = control.findResourceFile('live_tree_export.html', 'templates/exports', prefPath) jsPath = control.findResourceFile('live_tree_export.js', 'templates/exports', prefPath) cssPath = control.findResourceFile('live_tree_export.css', 'templates/exports', prefPath) if not htmlPath or not jsPath or not cssPath: QApplication.restoreOverrideCursor() QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - export template files not found.\n' 'Check your TreeLine installation.')) return False if ExportDialog.exportWhat == ExportDialog.entireTree: fileData = self.structure.fileData() else: self.structure = treestructure.TreeStructure(topNodes=self. selectedNodes, addSpots=False) fileData = self.structure.fileData() if ExportDialog.exportWhat == ExportDialog.selectNode: topNodeIds = set([node.uId for node in self.structure.childList]) nodeData = [data for data in fileData['nodes'] if data['uid'] in topNodeIds] for data in nodeData: data['children'] = [] fileData['nodes'] = nodeData with htmlPath.open(encoding='utf-8') as htmlIn: with pathObj.open('w', encoding='utf-8') as htmlOut: for line in htmlIn: if 'stylesheet' in line: htmlOut.write('') elif '' in line: line = re.sub(r'<title>.*', '{0}'. format(pathObj.stem), line) htmlOut.write(line) elif 'application/json' in line: htmlOut.write(line) json.dump(fileData, htmlOut, indent=2, sort_keys=True) elif 'dataFileName' in line: htmlOut.write(line) with jsPath.open(encoding='utf-8') as jsIn: htmlOut.write(jsIn.read()) elif 'script src=' not in line: htmlOut.write(line) return True def exportTextTitles(self, pathObj=None): """Export tabbed title text, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export Text Titles'), 'txt') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.entireTree: self.selectedSpots = self.structure.rootSpots() if ExportDialog.exportWhat == ExportDialog.selectNode: lines = [spot.nodeRef.title(spot) for spot in self.selectedSpots] else: treeView = (globalref.mainControl.activeControl.activeWindow. treeView) lines = [] for rootSpot in self.selectedSpots: for spot, level in rootSpot.levelSpotDescendantGen(treeView, ExportDialog.includeRoot, None, ExportDialog.openOnly): lines.append('\t' * level + spot.nodeRef.title(spot)) with pathObj.open('w', encoding=globalref.localTextEncoding) as f: f.writelines([(line + '\n') for line in lines]) return True def exportTextPlain(self, pathObj=None): """Export unformatted text for all output, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export Plain Text'), 'txt') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.entireTree: self.selectedSpots = self.structure.rootSpots() lines = [] if ExportDialog.exportWhat == ExportDialog.selectNode: for rootSpot in self.selectedSpots: lines.extend(rootSpot.nodeRef.output(True, False, rootSpot)) if rootSpot.nodeRef.formatRef.spaceBetween: lines.append('') else: treeView = (globalref.mainControl.activeControl.activeWindow. treeView) for rootSpot in self.selectedSpots: for spot, level in rootSpot.levelSpotDescendantGen(treeView, ExportDialog.includeRoot, None, ExportDialog.openOnly): lines.extend(spot.nodeRef.output(True, False, spot)) if spot.nodeRef.formatRef.spaceBetween: lines.append('') with pathObj.open('w', encoding=globalref.localTextEncoding) as f: f.writelines([(line + '\n') for line in lines]) return True def exportTextTableMultiCsv(self, pathObj=None): """Export descendant CSV delimited text table with level numbers. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export Text Tables'), 'csv') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.entireTree: self.selectedSpots = self.structure.rootSpots() treeView = (globalref.mainControl.activeControl.activeWindow.treeView) types = set() headings = [] for rootSpot in self.selectedSpots: for spot, level in rootSpot.levelSpotDescendantGen(treeView, ExportDialog.includeRoot, None, ExportDialog.openOnly): nodeFormat = spot.nodeRef.formatRef if nodeFormat not in types: for fieldName in nodeFormat.fieldNames(): if fieldName not in headings: headings.append(fieldName) types.add(nodeFormat) lines = [['Level'] + headings] for rootSpot in self.selectedSpots: for spot, level in rootSpot.levelSpotDescendantGen(treeView, ExportDialog.includeRoot, None, ExportDialog.openOnly): newLine = [spot.nodeRef.data.get(head, '') for head in headings] lines.append([repr(level)] + newLine) with pathObj.open('w', newline='', encoding=globalref.localTextEncoding) as f: writer = csv.writer(f) writer.writerows(lines) return True def exportTextTableCsv(self, pathObj=None): """Export child CSV delimited text table, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export Text Tables'), 'csv') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.selectNode: nodeList = self.selectedNodes else: nodeList = [] for node in self.selectedNodes: nodeList.extend(node.childList) types = set() headings = [] for node in nodeList: nodeFormat = node.formatRef if nodeFormat not in types: for fieldName in nodeFormat.fieldNames(): if fieldName not in headings: headings.append(fieldName) types.add(nodeFormat) lines = [headings] for node in nodeList: lines.append([node.data.get(head, '') for head in headings]) with pathObj.open('w', newline='', encoding=globalref.localTextEncoding) as f: writer = csv.writer(f) writer.writerows(lines) return True def exportTextTableTab(self, pathObj=None): """Export child tab delimited text table, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export Text Tables'), 'txt') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.selectNode: nodeList = self.selectedNodes else: nodeList = [] for node in self.selectedNodes: nodeList.extend(node.childList) types = set() headings = [] for node in nodeList: nodeFormat = node.formatRef if nodeFormat not in types: for fieldName in nodeFormat.fieldNames(): if fieldName not in headings: headings.append(fieldName) types.add(nodeFormat) lines = ['\t'.join(headings)] for node in nodeList: lines.append('\t'.join([node.data.get(head, '') for head in headings])) with pathObj.open('w', encoding=globalref.localTextEncoding) as f: f.writelines([(line + '\n') for line in lines]) return True def exportOldTreeLine(self, pathObj=None): """Export old TreeLine version (2.0.x), use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export TreeLine Subtree'), 'trl') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat != ExportDialog.entireTree: self.structure = treestructure.TreeStructure(topNodes=self. selectedNodes, addSpots=False) addDescend = ExportDialog.exportWhat != ExportDialog.selectNode addChildren = addDescend if len(self.structure.childList) > 1: if not addDescend: addChildren = True self.structure = treestructure.TreeStructure(topNodes=self. structure.childList, addSpots=False) rootType = nodeformat.NodeFormat(treeformats.defaultTypeName, self.structure.treeFormats, addDefaultField=True) self.structure.treeFormats.addTypeIfMissing(rootType) root = treenode.TreeNode(self.structure. treeFormats[treeformats.defaultTypeName]) root.setTitle(treestructure.defaultRootTitle) self.structure.addNodeDictRef(root) root.childList = self.structure.childList self.structure.childList = [root] idDict = {} for node in self.structure.childList[0].descendantGen(): _setOldUniqueId(idDict, node) idDict = {i[1]: i[0] for i in idDict.items()} # reverse (new id keys) rootElement = _oldElementXml(self.structure.childList[0], self.structure, idDict, addChildren=addChildren, addDescend=addDescend) if __version__: rootElement.set('tlversion', __version__) rootElement.attrib.update(_convertOldPrintData(self.printData. fileData())) elementTree = ElementTree.ElementTree(rootElement) elementTree.write(str(pathObj), 'utf-8', True) return True def exportSubtree(self, pathObj=None): """Export TreeLine subtree, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export TreeLine Subtree'), 'trlnsave') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) self.structure = treestructure.TreeStructure(topNodes=self. selectedNodes, addSpots=False) fileData = self.structure.fileData() fileData['properties'].update(self.printData.fileData()) if ExportDialog.exportWhat == ExportDialog.selectNode: topNodeIds = set([node.uId for node in self.structure.childList]) nodeData = [data for data in fileData['nodes'] if data['uid'] in topNodeIds] for data in nodeData: data['children'] = [] fileData['nodes'] = nodeData indent = 3 if globalref.genOptions['PrettyPrint'] else 0 with pathObj.open('w', encoding='utf-8', newline='\n') as f: json.dump(fileData, f, indent=indent, sort_keys=True) return True def exportXmlGeneric(self, pathObj=None): """Export generic XML, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export Generic XML'), 'xml') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.entireTree: self.selectedNodes = self.structure.childList addBranches = ExportDialog.exportWhat != ExportDialog.selectNode if len(self.selectedNodes) > 1: rootElement = ElementTree.Element(treeformats.defaultTypeName) for node in self.selectedNodes: rootElement.append(_createGenericXml(node, addBranches)) else: rootElement = _createGenericXml(self.selectedNodes[0], addBranches) elementTree = ElementTree.ElementTree(rootElement) elementTree.write(str(pathObj), 'utf-8', True) return True def exportOdfText(self, pathObj=None): """Export an ODF text file, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export ODF Text'), 'odt') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.entireTree: self.selectedSpots = self.structure.rootSpots() addBranches = ExportDialog.exportWhat != ExportDialog.selectNode for prefix, uri in _odfNamespace.items(): ElementTree.register_namespace(prefix, uri) versionAttr = {'office:version': '1.0'} fontInfo = QFontInfo(globalref.mainControl.activeControl. activeWindow.editorSplitter.widget(0). font()) fontAttr = {'style:font-pitch': 'fixed' if fontInfo.fixedPitch() else 'variable', 'style:name': fontInfo.family(), 'svg:font-family': fontInfo.family()} fontElem = _addOdfElement('office:font-face-decls') _addOdfElement('style:font-face', fontElem, fontAttr) fontSizeDelta = 2 contentRoot = _addOdfElement('office:document-content', attr=versionAttr) contentRoot.append(fontElem) contentBodyElem = _addOdfElement('office:body', contentRoot) contentTextElem = _addOdfElement('office:text', contentBodyElem) maxLevel = 0 for spot in self.selectedSpots: level = _addOdfText(spot, contentTextElem, addBranches) maxLevel = max(level, maxLevel) manifestRoot = _addOdfElement('manifest:manifest') _addOdfElement('manifest:file-entry', manifestRoot, {'manifest:media-type': 'application/vnd.oasis.opendocument.text', 'manifest:full-path': '/'}) _addOdfElement('manifest:file-entry', manifestRoot, {'manifest:media-type': 'text/xml', 'manifest:full-path': 'content.xml'}) _addOdfElement('manifest:file-entry', manifestRoot, {'manifest:media-type': 'text/xml', 'manifest:full-path': 'styles.xml'}) styleRoot = _addOdfElement('office:document-styles', attr=versionAttr) styleRoot.append(fontElem) stylesElem = _addOdfElement('office:styles', styleRoot) defaultStyleElem = _addOdfElement('style:default-style', stylesElem, {'style:family': 'paragraph'}) _addOdfElement('style:paragraph-properties', defaultStyleElem, {'style:writing-mode': 'page'}) _addOdfElement('style:text-properties', defaultStyleElem, {'fo:font-size': '{0}pt'.format(fontInfo.pointSize()), 'fo:hyphenate': 'false', 'style:font-name': fontInfo.family()}) _addOdfElement('style:style', stylesElem, {'style:name': 'Standard', 'style:class': 'text', 'style:family': 'paragraph'}) bodyStyleElem = _addOdfElement('style:style', stylesElem, {'style:name': 'Text_20_body', 'style:display-name': 'Text body', 'style:class': 'text', 'style:family': 'paragraph', 'style:parent-style-name': 'Standard'}) _addOdfElement('style:paragraph-properties', bodyStyleElem, {'fo:margin-bottom': '6.0pt'}) headStyleElem = _addOdfElement('style:style', stylesElem, {'style:name': 'Heading', 'style:class': 'text', 'style:family': 'paragraph', 'style:next-style-name': 'Text_20_body', 'style:parent-style-name': 'Standard'}) _addOdfElement('style:paragraph-properties', headStyleElem, {'fo:keep-with-next': 'always', 'fo:margin-bottom': '6.0pt', 'fo:margin-top': '12.0pt'}) _addOdfElement('style:text-properties', headStyleElem, {'fo:font-size': '{0}pt'.format(fontInfo.pointSize() + fontSizeDelta), 'style:font-name': fontInfo.family()}) outlineStyleElem = _addOdfElement('text:outline-style') for level in range(1, maxLevel + 1): size = fontInfo.pointSize() if level <= 2: size += 2 * fontSizeDelta elif level <= 4: size += fontSizeDelta levelStyleElem = _addOdfElement('style:style', stylesElem, {'style:name': 'Heading_20_{0}'.format(level), 'style:display-name': 'Heading {0}'.format(level), 'style:class': 'text', 'style:family': 'paragraph', 'style:parent-style-name': 'Heading', 'style:default-outline-level': '{0}'.format(level)}) levelTextElem = _addOdfElement('style:text-properties', levelStyleElem, {'fo:font-size': '{0}pt'.format(size), 'fo:font-weight': 'bold'}) if level % 2 == 0: levelTextElem.set('fo:font-style', 'italic') _addOdfElement('text:outline-level-style', outlineStyleElem, {'text:level': '{0}'.format(level), 'style:num-format': ''}) stylesElem.append(outlineStyleElem) autoStyleElem = _addOdfElement('office:automatic-styles', styleRoot) pageLayElem = _addOdfElement('style:page-layout', autoStyleElem, {'style:name': 'pm1'}) _addOdfElement('style:page-layout-properties', pageLayElem, {'fo:margin-bottom': '0.75in', 'fo:margin-left': '0.75in', 'fo:margin-right': '0.75in', 'fo:margin-top': '0.75in', 'fo:page-height': '11in', 'fo:page-width': '8.5in', 'style:print-orientation': 'portrait'}) masterStyleElem = _addOdfElement('office:master-styles', styleRoot) _addOdfElement('style:master-page', masterStyleElem, {'style:name': 'Standard', 'style:page-layout-name': 'pm1'}) with zipfile.ZipFile(str(pathObj), 'w', zipfile.ZIP_DEFLATED) as odfZip: _addElemToZip(odfZip, contentRoot, 'content.xml') _addElemToZip(odfZip, manifestRoot, 'META-INF/manifest.xml') _addElemToZip(odfZip, styleRoot, 'styles.xml') return True def exportBookmarksHtml(self, pathObj=None): """Export HTML format bookmarks, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export HTML Bookmarks'), 'html') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.entireTree: self.selectedNodes = self.structure.childList addBranches = ExportDialog.exportWhat != ExportDialog.selectNode title = _bookmarkTitle if len(self.selectedNodes) == 1 and addBranches: title = self.selectedNodes[0].title() self.selectedNodes = self.selectedNodes[0].childList lines = ['', '', '{0}'.format(title), '

    {0}

    '.format(title)] for node in self.selectedNodes: lines.extend(_exportHtmlBookmarks(node, addBranches)) with pathObj.open('w', encoding='utf-8') as f: f.writelines([(line + '\n') for line in lines]) return True def exportBookmarksXbel(self, pathObj=None): """Export XBEL format bookmarks, use ExportDialog options. Prompt user for path if not given in argument. Return True on successful export. Arguments: pathObj -- use if given, otherwise prompt user """ if not pathObj: pathObj = self.getFileName(_('TreeLine - Export XBEL Bookmarks'), 'xml') if not pathObj: return False QApplication.setOverrideCursor(Qt.WaitCursor) if ExportDialog.exportWhat == ExportDialog.entireTree: self.selectedNodes = self.structure.childList addBranches = ExportDialog.exportWhat != ExportDialog.selectNode title = _bookmarkTitle if len(self.selectedNodes) == 1 and addBranches: title = self.selectedNodes[0].title() self.selectedNodes = self.selectedNodes[0].childList rootElem = ElementTree.Element('xbel') titleElem = ElementTree.Element('title') titleElem.text = title rootElem.append(titleElem) for node in self.selectedNodes: rootElem.append(_exportXbel(node, addBranches)) elementTree = ElementTree.ElementTree(rootElem) with pathObj.open('wb') as f: f.write(b'\n') elementTree.write(f, 'utf-8', False) return True def _setHtmlDirectories(node, pathDict, parentPath, siblingNames, addSuffix=True): """Recursively create path obj for node and add to the path dict by uId. Arguments: node -- the node to create a path object for pathDict -- the dict of paths by uId for adding an entry parentPath -- the path of the parent node siblingNames -- set of already used sibling names addSuffix -- add '.html' suffix to file names if True """ name = node.title() maxLength = 32 if len(name) > maxLength: pos = name.rfind(' ', maxLength // 2, maxLength + 1) if pos < 0: pos = maxLength name = name[:pos] name = name.replace(' ', '_') name = _idReplaceCharsRe.sub('', name) if not name: name = 'id' elif not 'a' <= name.lower() <= 'z': name = 'id_' + name origName = name i = 1 while name in siblingNames: name = origName + '_' + repr(i) i += 1 siblingNames.add(name) pathObj = parentPath / name filePathObj = pathObj.with_suffix('.html') if addSuffix else pathObj pathDict[node.uId] = filePathObj siblings = set() for child in node.childList: _setHtmlDirectories(child, pathDict, pathObj, siblings, addSuffix) def _writeHtmlPage(node, parent, grandparent, pathDict, level=0): """Write web pages with navigation for this node and descendents. Arguments: node -- the node to write the page for parent -- the parent node (or None) grandparent -- the grandparent node (or None) pathDict -- the dict of paths by uId level -- indicates the depth and how far up the css file is """ lines = ['', '', '', '', ''.format('../' * level), '{0}'.format(node.title()), '', '', '', '
    ']) outputLines = [line + '
    ' for line in node.output()] if node.formatRef.siblingPrefix: outputLines[0] = node.formatRef.siblingPrefix + outputLines[0] if node.formatRef.siblingSuffix: outputLines[-1] += node.formatRef.siblingSuffix for i in range(len(outputLines)): startPos = 0 while True: match = _genLinkRe.search(outputLines[i], startPos) if not match: break addr = match.group(1) if addr.startswith('#'): pathObj = pathDict.get(addr[1:], None) if pathObj: relPath = os.path.relpath(str(pathObj), nodeDir) outputLines[i] = (outputLines[i][:match.start(1)] + relPath + outputLines[i][match.end(1):]) elif urltools.isRelative(addr): outputLines[i] = (outputLines[i][:match.start(1)] + '../' * level + addr + outputLines[i][match.end(1):]) startPos = match.start(1) startPos = 0 while True: match = _imgLinkRe.search(outputLines[i], startPos) if not match: break addr = match.group(1) if not addr.startswith('#') and urltools.isRelative(addr): outputLines[i] = (outputLines[i][:match.start(1)] + '../' * level + addr + outputLines[i][match.end(1):]) startPos = match.start(1) lines.extend(outputLines) lines.extend(['
    ', '', '']) with pathDict[node.uId].open('w', encoding='utf-8') as f: f.writelines([(line + '\n') for line in lines]) if node.childList: dirObj = pathDict[node.uId].with_suffix('') if not dirObj.is_dir(): dirObj.mkdir(0o755) os.chdir(str(dirObj)) for child in node.childList: _writeHtmlPage(child, node, parent, pathDict, level + 1) os.chdir('..') def _writeHtmlTable(node, parent, pathDict, level=1): """Write web pages with tables for child data to nested directories. Arguments: node -- the node to write the page for parent -- the parent node (or None) pathDict -- the dict of paths by uId level -- the depth and how far up local links should point """ if not node.childList: return dirObj = pathDict[node.uId] if not dirObj.is_dir(): dirObj.mkdir(0o755) os.chdir(str(dirObj)) title = node.title() lines = ['', '', '', '', '{0}'.format(title), '', ''] if ExportDialog.addHeader: headerText = (globalref.mainControl.activeControl.printData. formatHeaderFooter(True)) if headerText: lines.append(headerText) lines.append('

    {0}

    '.format(title)) if parent: lines.append('

    {0}: {1}' '

    '.format(_('Parent'), parent.title())) lines.extend(['', '']) lines.extend([''.format(name) for name in node.childList[0].formatRef.fieldNames()]) lines.append('') for child in node.childList: cellList = [field.outputText(child, False, False, True) for field in child.formatRef.fields()] for i in range(len(cellList)): startPos = 0 while True: match = _genLinkRe.search(cellList[i], startPos) if not match: break addr = match.group(1) if addr.startswith('#'): pathObj = pathDict.get(addr[1:], None) if pathObj: name = pathObj.stem pathObj = pathObj / '..' / 'index.html' relPath = os.path.relpath(str(pathObj), str(dirObj)) relPath += '#' + name cellList[i] = (cellList[i][:match.start(1)] + relPath + cellList[i][match.end(1):]) elif urltools.isRelative(addr): cellList[i] = (cellList[i][:match.start(1)] + '../' * level + addr + cellList[i][match.end(1):]) startPos = match.start(1) startPos = 0 while True: match = _imgLinkRe.search(cellList[i], startPos) if not match: break addr = match.group(1) if not addr.startswith('#') and urltools.isRelative(addr): cellList[i] = (cellList[i][:match.start(1)] + '../' * level + addr + cellList[i][match.end(1):]) startPos = match.start(1) if child.childList: cellList[0] = ('{1}'. format(pathDict[child.uId].stem, cellList[0])) cellList[0] = '{1}'.format(pathDict[child.uId].stem, cellList[0]) lines.extend([''.format(cell) for cell in cellList]) lines.append('') lines.extend(['', '
    {0}
    {0}
    ']) if ExportDialog.addHeader: footerText = (globalref.mainControl.activeControl.printData. formatHeaderFooter(False)) if footerText: lines.append(footerText) lines.extend(['', '']) with open('index.html', 'w', encoding='utf-8') as f: f.writelines([(line + '\n') for line in lines]) for child in node.childList: _writeHtmlTable(child, node, pathDict, level + 1) os.chdir('..') def _oldElementXml(node, structRef, idDict, skipTypeFormats=None, extraFormats=True, addChildren=True, addDescend=True): """Return an Element object with the XML for this node's branch. Arguments: node -- the root node to save structRef -- a ref to the tree structure idDict -- a dict of new IDs to old IDs skipTypeFormats -- a set of node format types not included in XML extraFormats -- if True, includes unused format info addChildren -- if True, include data for the first level of children addDescend -- if True, add lower descendant nodes """ if skipTypeFormats == None: skipTypeFormats = set() nodeFormat = node.formatRef addFormat = nodeFormat.name not in skipTypeFormats element = ElementTree.Element(nodeFormat.name, {'item':'y'}) # add line feeds to make output somewhat readable element.tail = '\n' element.text = '\n' element.set('uniqueid', idDict[node.uId]) if addFormat: element.attrib.update(_convertOldNodeFormat(nodeFormat.storeFormat())) skipTypeFormats.add(nodeFormat.name) firstField = True for field in nodeFormat.fields(): text = node.data.get(field.name, '') if text or addFormat: fieldElement = ElementTree.SubElement(element, field.name) fieldElement.tail = '\n' if field.typeName in ('Date', 'DateTime'): text = text.replace('-', '/') if (field.typeName in ('Time', 'DateTime') and text.endswith('.000000')): text = text[:-7] linkCount = 0 startPos = 0 while True: match = _intLinkRe.search(text, startPos) if not match: break uId = idDict.get(match.group(1), '') if uId: text = text[:match.start(1)] + uId + text[match.end(1):] linkCount += 1 startPos = match.start(1) if linkCount: fieldElement.attrib['linkcount'] = repr(linkCount) fieldElement.text = text if addFormat: fieldElement.attrib.update(_convertOldFieldFormat(field. formatData())) if firstField: fieldElement.attrib['idref'] = 'y' firstField = False if addChildren: for child in node.childList: element.append(_oldElementXml(child, structRef, idDict, skipTypeFormats, False, addChildren=addDescend)) nodeFormats = [] if extraFormats: # write format info for unused formats nodeFormats = list(structRef.treeFormats.values()) if structRef.treeFormats.fileInfoFormat.fieldFormatModified: nodeFormats.append(structRef.treeFormats.fileInfoFormat) for nodeFormat in nodeFormats: if nodeFormat.name not in skipTypeFormats: formatElement = ElementTree.SubElement(element, nodeFormat.name, {'item':'n'}) formatElement.tail = '\n' formatElement.attrib.update(_convertOldNodeFormat(nodeFormat. storeFormat())) firstField = True for field in nodeFormat.fields(): fieldElement = ElementTree.SubElement(formatElement, field.name) fieldElement.tail = '\n' fieldElement.attrib.update(_convertOldFieldFormat(field. formatData())) if firstField: fieldElement.attrib['idref'] = 'y' firstField = False return element def _setOldUniqueId(idDict, node): """Set an old TreeLine unique ID for this node amd add to dict. Arguments: idDict -- a dict of old IDs to new IDs. node -- the node to give an old ID """ nodeFormat = node.formatRef idField = next(iter(nodeFormat.fieldDict.values())) uId = idField.outputText(node, True, True, nodeFormat.formatHtml) uId = uId.strip().split('\n', 1)[0] maxLength = 50 if len(uId) > maxLength: pos = uId.rfind(' ', maxLength // 2, maxLength + 1) if pos < 0: pos = maxLength uId = uId[:pos] uId = uId.replace(' ', '_').lower() uId = _idReplaceCharsRe.sub('', uId) if not uId: uId = 'id_1' elif not 'a' <= uId <= 'z': uId = 'id_' + uId if uId in idDict: if uId == 'id_1': uId = 'id' i = 1 while uId + '_' + repr(i) in idDict: i += 1 uId = uId + '_' + repr(i) idDict[uId] = node.uId def _convertOldNodeFormat(attrib): """Return old XML node format attributes from current data. Arguments: attrib -- current node format data attributes """ if 'spacebetween' in attrib and not attrib['spacebetween']: attrib['spacebetween'] = 'n' for key in ('formathtml', 'bullets', 'tables'): if key in attrib and attrib[key]: attrib[key] = 'y' attrib['line0'] = attrib.get('titleline', '') del attrib['titleline'] for i, line in enumerate(attrib['outputlines'], 1): attrib['line' + repr(i)] = line del attrib['outputlines'] del attrib['formatname'] del attrib['fields'] return attrib def _convertOldFieldFormat(attrib): """Return old XML field format attributes from current data. Arguments: attrib -- current field format data attributes """ if 'fieldtype' in attrib: attrib['type'] = attrib['fieldtype'] del attrib['fieldtype'] for key in ('lines', 'sortkeynum'): if key in attrib: attrib[key] = repr(attrib[key]) if 'sortkeyfwd' in attrib and not attrib['sortkeyfwd']: attrib['sortkeydir'] = 'r' if 'evalhtml' in attrib: attrib['evalhtml'] = 'y' if attrib['evalhtml'] else 'n' if attrib['type'] in ('Date', 'Time', 'DateTime'): fieldFormat = attrib.get('format', '') if fieldFormat: fieldFormat = fieldFormat.replace('%A', 'dddd') fieldFormat = fieldFormat.replace('%a', 'ddd') fieldFormat = fieldFormat.replace('%d', 'dd') fieldFormat = fieldFormat.replace('%-d', 'd') fieldFormat = fieldFormat.replace('%B', 'MMMM') fieldFormat = fieldFormat.replace('%b', 'MMM') fieldFormat = fieldFormat.replace('%m', 'MM') fieldFormat = fieldFormat.replace('%-m', 'M') fieldFormat = fieldFormat.replace('%Y', 'yyyy') fieldFormat = fieldFormat.replace('%y', 'yy') fieldFormat = fieldFormat.replace('%H', 'HH') fieldFormat = fieldFormat.replace('%-H', 'H') fieldFormat = fieldFormat.replace('%I', 'hh') fieldFormat = fieldFormat.replace('%-I', 'h') fieldFormat = fieldFormat.replace('%M', 'mm') fieldFormat = fieldFormat.replace('%-M', 'm') fieldFormat = fieldFormat.replace('%S', 'ss') fieldFormat = fieldFormat.replace('%-S', 's') fieldFormat = fieldFormat.replace('%f', 'zzz') fieldFormat = fieldFormat.replace('%p', 'AP') attrib['format'] = fieldFormat del attrib['fieldname'] return attrib def _convertOldPrintData(attrib): """Return old XML print data attributes from current print data. Arguments: attrib -- current print data attributes """ for key in ('printlines', 'printwidowcontrol', 'printportrait'): if key in attrib and not attrib[key]: attrib[key] = 'n' for key in ('printindentfactor', 'printpaperwidth', 'printpaperheight', 'printheadermargin', 'printfootermargin', 'printcolumnspace', 'printnumcolumns'): if key in attrib: attrib[key] = repr(attrib[key]) if 'printmargins' in attrib: attrib['printmargins'] = ' '.join([repr(margin) for margin in attrib['printmargins']]) return attrib def _createGenericXml(node, addChildren=True): """Return an ElementTree element with generic XML from this branch. Called recursively for children if addChildren is True. Arguments: node -- the node to export addChildren -- add branch if True """ nodeFormat = node.formatRef element = ElementTree.Element(nodeFormat.name) element.tail = '\n' for fieldName in nodeFormat.fieldNames(): text = node.data.get(fieldName, '') if text and fieldName != imports.genericXmlTextFieldName: element.set(fieldName, text) if imports.genericXmlTextFieldName in nodeFormat.fieldDict: text = node.data.get(imports.genericXmlTextFieldName, '') if text: element.text = text if addChildren and node.childList: if not text: element.text = '\n' for child in node.childList: element.append(_createGenericXml(child)) return element def _addElemToZip(destZip, rootElem, fileName): """Adds ElementTree root elements to the given zip file. Arguments: destZip -- the destination zip file rootElem -- the root element tree item to add fileName -- the file name or path in the zip file """ elemTree = ElementTree.ElementTree(rootElem) with io.BytesIO() as output: elemTree.write(output, 'utf-8', True) destZip.writestr(fileName, output.getvalue()) def _addOdfElement(name, parent=None, attr=None): """Shortcut function to add elements to the ElementTree. Converts names and attr keys from short version (with ':') to the full URI. Returns the new element. Arguments: name -- the element tag parent -- new element is added here if given attr -- a dict of the element's attrbutes """ if ':' in name: prefix, name = name.split(':', 1) name = '{{{0}}}{1}'.format(_odfNamespace[prefix], name) newAttr = {} if attr: for key, value in attr.items(): if ':' in key: prefix, key = key.split(':', 1) key = '{{{0}}}{1}'.format(_odfNamespace[prefix], key) newAttr[key] = value elem = ElementTree.Element(name, newAttr) elem.tail = '\n' if parent is not None: parent.append(elem) return elem def _addOdfText(spot, parentElem, addChildren=True, level=1, maxLevel=1): """Add heading and text elements to the parent element tree element. Called recursively for children if addChildren is True. Returns the maximum indent level used for this branch. Arguments: spot -- the spot to export parentElem -- the parent element tree element to add to addChildren -- add branch if True level -- the current tree indent level maxLevel -- the previous max indent level """ headElem = _addOdfElement('text:h', parentElem, {'text:outline-level': '{0}'.format(level), 'text:style-name': 'Heading_20_{0}'.format(level)}) headElem.text = spot.nodeRef.title(spot) output = spot.nodeRef.output(True, False, spot) if output and output[0] == spot.nodeRef.title(spot): del output[0] # remove first line if same as title for line in output: textElem = _addOdfElement('text:p', parentElem, {'text:outline-level': '{0}'.format(level), 'text:style-name': 'Text_20_body'}) textElem.text = line if addChildren and spot.nodeRef.childList: for child in spot.childSpots(): childlevel = _addOdfText(child, parentElem, True, level + 1, maxLevel) maxLevel = max(childlevel, maxLevel) else: maxLevel = max(level, maxLevel) return maxLevel def _exportHtmlBookmarks(node, addChildren=True): """Return a text list ith descendant bookmarks in Mozilla format. Called recursively for children if addChildren is True. Arguments: node -- the node to export addChildren -- add branch if True """ title = node.title() if not node.childList: nodeFormat = node.formatRef field = _findLinkField(nodeFormat) if field: linkMatch = fieldformat.linkRegExp.search(node.data. get(field.name, '')) if linkMatch: link = linkMatch.group(1) return ['
    {1}'.format(link, title)] elif (len(nodeFormat.fieldDict) == 1 and not node.data.get(nodeFormat.fieldNames()[0], '')): return ['
    '] result = ['

    {0}

    '.format(title)] if addChildren: result.append('

    ') for child in node.childList: result.extend(_exportHtmlBookmarks(child)) result.append('

    ') return result def _exportXbel(node, addChildren=True): """Return an ElementTree element with XBEL bookmarks from this branch. Called recursively for children if addChildren is True. Arguments: node -- the node to export addChildren -- add branch if True """ titleElem = ElementTree.Element('title') titleElem.text = node.title() if not node.childList: nodeFormat = node.formatRef field = _findLinkField(nodeFormat) if field: linkMatch = fieldformat.linkRegExp.search(node.data. get(field.name, '')) if linkMatch: link = linkMatch.group(1) element = ElementTree.Element('bookmark', {'href': link}) element.append(titleElem) element.tail = '\n' return element elif (len(nodeFormat.fieldDict) == 1 and not node.data.get(nodeFormat.fieldNames()[0], '')): element = ElementTree.Element('separator') element.tail = '\n' return element element = ElementTree.Element('folder') element.append(titleElem) element.tail = '\n' if addChildren: for child in node.childList: element.append(_exportXbel(child)) return element def _findLinkField(nodeFormat): """Return the field most likely to contain a bookmark URL. Return None if there are no matches. Arguments: nodeFormat -- the format to find a field in """ availFields = [field for field in nodeFormat.fieldDict.values() if field.typeName == 'ExternalLink'] if not availFields: return None bestFields = [field for field in availFields if field.name.lower() == imports.bookmarkLinkFieldName.lower()] if bestFields: return bestFields[0] return availFields[0] class ExportDialog(QWizard): """Dialog/wizard for setting file export type and options. """ typePage, subtypePage, optionPage = range(3) entireTree, selectBranch, selectNode = range(3) exportWhat = entireTree includeRoot = False openOnly = False addHeader = False numColumns = 1 navPaneLevels = 2 exportTypes = ['html', 'text', 'treeline', 'xml', 'odf', 'bookmarks'] currentType = 'html' exportTypeDescript = {'html': _('&HTML'), 'text': _('&Text'), 'treeline': _('Tree&Line'), 'xml': _('&XML (generic)'), 'odf': _('&ODF Outline'), 'bookmarks': _('Book&marks')} exportSubtypes = {'html': ['htmlSingle', 'htmlNavSingle','htmlPages', 'htmlTables', 'htmlLiveLink', 'htmlLiveSingle'], 'text': ['textTitles', 'textPlain', 'textTableMultiCsv', 'textTableCsv', 'textTableTab'], 'treeline': ['oldTreeLine', 'treeLineSubtree'], 'xml': ['xmlGeneric'], 'odf': ['odfText'], 'bookmarks': ['bookmarksHtml', 'bookmarksXbel']} currentSubtype = 'htmlSingle' subtypeDescript = {'htmlSingle': _('&Single HTML page'), 'htmlNavSingle': _('Single &HTML page with ' 'navigation pane'), 'htmlPages': _('Multiple HTML &pages with ' 'navigation pane'), 'htmlTables': _('Multiple HTML &data tables'), 'htmlLiveLink': _('Live tree view, linked to ' 'TreeLine file (for web server)'), 'htmlLiveSingle': _('Live tree view, single file ' '(embedded data)'), 'textTitles': _('&Tabbed title text'), 'textPlain': _('&Unformatted output of all text'), 'textTableMultiCsv': _('&Comma delimited (CSV) table ' 'of descendants (level numbers)'), 'textTableCsv': _('Comma &delimited (CSV) table ' 'of children (single level)'), 'textTableTab': _('Tab &delimited table of children ' '(&single level)'), 'oldTreeLine': _('&Old TreeLine (2.0.x)'), 'treeLineSubtree': _('&TreeLine Subtree'), 'bookmarksHtml': _('&HTML format bookmarks'), 'bookmarksXbel': _('&XBEL format bookmarks')} disableEntireTree = {'textTableCsv', 'textTableTab', 'treeLineSubtree'} disableSelBranches = {'htmlLiveLink'} disableSelNodes = {'htmlNavSingle', 'htmlPages', 'htmlTables', 'htmlLiveLink', 'textTableMultiCsv'} enableRootNode = {'htmlSingle', 'htmlNavSingle', 'textTitles', 'textPlain', 'textTableMultiCsv', 'ODF'} forceRootNodeOff = {'textTableCsv', 'textTableTab'} enableOpenOnly = {'htmlSingle', 'htmlNavSingle', 'textTitles', 'textPlain', 'textTableMultiCsv', 'ODF'} enableHeader = {'htmlSingle', 'htmlNavSingle', 'htmlTables'} enableColumns = {'htmlSingle'} enableNavLevels = {'htmlNavSingle'} def __init__(self, selectionAvail=True, parent=None): """Initialize the export wizard. Arguments: selectionAvail -- false if no nodes or branches are selected parent -- the parent window """ super().__init__(parent, Qt.Dialog) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('File Export')) self.setWizardStyle(QWizard.ClassicStyle) self.setPage(ExportDialog.typePage, ExportDialogTypePage()) self.setPage(ExportDialog.subtypePage, ExportDialogSubtypePage()) self.setPage(ExportDialog.optionPage, ExportDialogOptionPage(selectionAvail)) class ExportDialogTypePage(QWizardPage): """A wizard page for selecting the main export type. """ def __init__(self, parent=None): """Initialize the export wizard page. Arguments: parent -- parent widget, set automatically by addPage or setPage """ super().__init__(parent) topLayout = QVBoxLayout(self) self.setLayout(topLayout) self.setTitle(_('Choose export format type')) typeButtons = QButtonGroup(self) for id, exportType in enumerate(ExportDialog.exportTypes): button = QRadioButton(ExportDialog. exportTypeDescript[exportType]) typeButtons.addButton(button, id) topLayout.addWidget(button) if exportType == ExportDialog.currentType: button.setChecked(True) typeButtons.buttonClicked[int].connect(self.setCurrentType) def setCurrentType(self, buttonID): """Set the saved current type value based on a button click. Also sets the subtype to a default value. Arguments: buttonId -- the ID number of the button that was clicked """ ExportDialog.currentType = ExportDialog.exportTypes[buttonID] ExportDialog.currentSubtype = (ExportDialog. exportSubtypes[ExportDialog.currentType][0]) def nextId(self): """Return the ID for the next page in the wizard sequence. """ if len(ExportDialog.exportSubtypes[ExportDialog.currentType]) > 1: return ExportDialog.subtypePage return ExportDialog.optionPage class ExportDialogSubtypePage(QWizardPage): """A wizard page for selecting the export subtype. """ def __init__(self, parent=None): """Initialize the export wizard page. Arguments: parent -- parent widget, set automatically by addPage or setPage """ super().__init__(parent) topLayout = QVBoxLayout(self) self.setLayout(topLayout) self.setTitle(_('Choose export format subtype')) self.subtypeButtons = QButtonGroup(self) self.subtypeButtons.buttonClicked[int].connect(self.setCurrentSubtype) def initializePage(self): """Add buttons to this page based on current settings. """ topLayout = self.layout() # remove old buttons from a previously set subtype for button in self.subtypeButtons.buttons(): self.subtypeButtons.removeButton(button) topLayout.removeWidget(button) button.deleteLater() for id, subtype in enumerate(ExportDialog. exportSubtypes[ExportDialog.currentType]): button = QRadioButton(ExportDialog.subtypeDescript[subtype]) self.subtypeButtons.addButton(button, id) topLayout.addWidget(button) if subtype == ExportDialog.currentSubtype: button.setChecked(True) def setCurrentSubtype(self, buttonId): """Set the saved current subtype value based on a button click. Arguments: buttonId -- the ID number of the button that was clicked """ availSubtypes = ExportDialog.exportSubtypes[ExportDialog.currentType] ExportDialog.currentSubtype = availSubtypes[buttonId] class ExportDialogOptionPage(QWizardPage): """A wizard page for selecting other export options. """ def __init__(self, selectionAvail=True, parent=None): """Initialize the export wizard page. Arguments: selectionAvail -- false if no nodes or branches are selected parent -- parent widget, set automatically by addPage or setPage """ super().__init__(parent) self.selectionAvail = selectionAvail topLayout = QVBoxLayout(self) self.setLayout(topLayout) self.setTitle(_('Choose export options')) whatGroupBox = QGroupBox(_('What to Export')) topLayout.addWidget(whatGroupBox) whatLayout = QVBoxLayout(whatGroupBox) self.whatButtons = QButtonGroup(self) treeButton = QRadioButton(_('&Entire tree')) self.whatButtons.addButton(treeButton, ExportDialog.entireTree) whatLayout.addWidget(treeButton) branchButton = QRadioButton(_('Selected &branches')) self.whatButtons.addButton(branchButton, ExportDialog.selectBranch) whatLayout.addWidget(branchButton) nodeButton = QRadioButton(_('Selected &nodes')) self.whatButtons.addButton(nodeButton, ExportDialog.selectNode) whatLayout.addWidget(nodeButton) self.whatButtons.button(ExportDialog.exportWhat).setChecked(True) self.whatButtons.buttonClicked[int].connect(self.setExportWhat) optionBox = QGroupBox(_('Other Options')) topLayout.addWidget(optionBox) optionLayout = QVBoxLayout(optionBox) self.rootButton = QCheckBox(_('&Include root nodes')) optionLayout.addWidget(self.rootButton) self.rootButton.setChecked(ExportDialog.includeRoot) self.rootButton.toggled.connect(self.setIncludeRoot) self.openOnlyButton = QCheckBox(_('&Only open node children')) optionLayout.addWidget(self.openOnlyButton) self.openOnlyButton.setChecked(ExportDialog.openOnly) self.openOnlyButton.toggled.connect(self.setOpenOnly) self.headerButton = QCheckBox(_('Include &print header && ' 'footer')) optionLayout.addWidget(self.headerButton) self.headerButton.setChecked(ExportDialog.addHeader) self.headerButton.toggled.connect(self.setAddHeader) columnLayout = QHBoxLayout() optionLayout.addLayout(columnLayout) self.numColSpin = QSpinBox() columnLayout.addWidget(self.numColSpin) self.numColSpin.setRange(1, 9) self.numColSpin.setMaximumWidth(40) self.numColSpin.setValue(ExportDialog.numColumns) self.colLabel = QLabel(_('&Columns')) columnLayout.addWidget(self.colLabel) self.colLabel.setBuddy(self.numColSpin) self.numColSpin.valueChanged.connect(self.setNumColumns) navLevelsLayout = QHBoxLayout() optionLayout.addLayout(navLevelsLayout) self.navLevelsSpin = QSpinBox() navLevelsLayout.addWidget(self.navLevelsSpin) self.navLevelsSpin.setRange(1, 9) self.navLevelsSpin.setMaximumWidth(40) self.navLevelsSpin.setValue(ExportDialog.navPaneLevels) self.navLevelsLabel = QLabel(_('Navigation pane &levels')) navLevelsLayout.addWidget(self.navLevelsLabel) self.navLevelsLabel.setBuddy(self.navLevelsSpin) self.navLevelsSpin.valueChanged.connect(self.setNavLevels) def initializePage(self): """Enable or disable controls based on current settings. """ subtype = ExportDialog.currentSubtype treeButton, branchButton, nodeButton = self.whatButtons.buttons() treeButton.setEnabled(subtype not in ExportDialog.disableEntireTree) branchButton.setEnabled(subtype not in ExportDialog.disableSelBranches and self.selectionAvail) nodeButton.setEnabled(subtype not in ExportDialog.disableSelNodes and self.selectionAvail) num = 0 while not self.whatButtons.checkedButton().isEnabled(): try: self.whatButtons.button(num).setChecked(True) except AttributeError: QMessageBox.warning(self, 'TreeLine', _('Must select nodes prior to export')) parent = self.parent() while parent: try: parent.reject() return except AttributeError: parent = parent.parent() num += 1 if (subtype in ExportDialog.enableRootNode and ExportDialog.exportWhat != ExportDialog.selectNode): self.rootButton.setEnabled(True) self.rootButton.setChecked(ExportDialog.includeRoot) else: self.rootButton.setEnabled(False) self.rootButton.setChecked(subtype not in ExportDialog.forceRootNodeOff) if (subtype in ExportDialog.enableOpenOnly and ExportDialog.exportWhat != ExportDialog.selectNode): self.openOnlyButton.setEnabled(True) else: self.openOnlyButton.setEnabled(False) self.openOnlyButton.setChecked(False) self.headerButton.setEnabled(subtype in ExportDialog.enableHeader) if subtype not in ExportDialog.enableHeader: self.headerButton.setChecked(False) columnsEnabled = subtype in ExportDialog.enableColumns self.numColSpin.setVisible(columnsEnabled) self.colLabel.setVisible(columnsEnabled) if not columnsEnabled: self.numColSpin.setValue(1) navLevelsEnabled = subtype in ExportDialog.enableNavLevels self.navLevelsSpin.setVisible(navLevelsEnabled) self.navLevelsLabel.setVisible(navLevelsEnabled) def setExportWhat(self, buttonNum): """Set what to export (all, branch, node) based on button group click. Arguments: buttonNum -- the ID number of the clicked button """ ExportDialog.exportWhat = buttonNum self.initializePage() def setIncludeRoot(self, checked): """Set whether root node is included based on a button click. Arguments: checked -- True if the check box is checked """ ExportDialog.includeRoot = checked def setOpenOnly(self, checked): """Set whether only open nodes are included based on a button click. Arguments: checked -- True if the check box is checked """ ExportDialog.openOnly = checked def setAddHeader(self, checked): """Set whether headers and footers are added based on a button click. Arguments: checked -- True if the check box is checked """ ExportDialog.addHeader = checked def setNumColumns(self, num): """Set number of columns based on a spin box change. Arguments: num -- the new spin box setting """ ExportDialog.numColumns = num def setNavLevels(self, num): """Set number of navigation pane levels based on a spin box change. Arguments: num -- the new spin box setting """ ExportDialog.navPaneLevels = num TreeLine/source/outputview.py0000644000175000017500000001367413363127527015374 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # outputview.py, provides a class for the data output view # # TreeLine, an information storage program # Copyright (C) 2017, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** from PyQt5.QtCore import Qt from PyQt5.QtGui import QPalette, QTextCursor from PyQt5.QtWidgets import QTextBrowser, QTextEdit import treeoutput import urltools import dataeditors import globalref class OutputView(QTextBrowser): """Class override for the data output view. Sets view defaults and updates the content. """ def __init__(self, treeView, isChildView=True, parent=None): """Initialize the output view. Arguments: treeView - the tree view, needed for the current selection model isChildView -- shows selected nodes if false, child nodes if true parent -- the parent main window """ super().__init__(parent) self.treeView = treeView self.isChildView = isChildView self.hideChildView = not globalref.genOptions['InitShowChildPane'] self.showDescendants = globalref.genOptions['InitShowDescendants'] self.setFocusPolicy(Qt.NoFocus) def updateContents(self): """Reload the view's content if the view is shown. Avoids update if view is not visible or has zero height or width. """ selSpots = self.treeView.selectionModel().selectedSpots() if self.isChildView: if (len(selSpots) > 1 or self.hideChildView or (selSpots and not selSpots[0].nodeRef.childList)): self.hide() return if not selSpots: # use top node childList from tree structure selSpots = [globalref.mainControl.activeControl.structure. structSpot()] elif not selSpots: self.hide() return self.show() if not self.isVisible() or self.height() == 0 or self.width() == 0: return if self.isChildView: if self.showDescendants: outputGroup = treeoutput.OutputGroup(selSpots, False, True) if outputGroup.hasPrefixes(): outputGroup.combineAllSiblings() outputGroup.addBlanksBetween() outputGroup.addAbsoluteIndents() else: outputGroup = treeoutput.OutputGroup(selSpots[0].childSpots()) outputGroup.addBlanksBetween() outputGroup.addSiblingPrefixes() else: outputGroup = treeoutput.OutputGroup(selSpots) outputGroup.addBlanksBetween() outputGroup.addSiblingPrefixes() self.setHtml('\n'.join(outputGroup.getLines())) self.setSearchPaths([str(globalref.mainControl.defaultPathObj(True))]) def setSource(self, url): """Called when a user clicks on a URL link. Selects an internal link or opens an external browser. Arguments: url -- the QUrl that is clicked """ name = url.toString() if name.startswith('#'): if not self.treeView.selectionModel().selectNodeById(name[1:]): super().setSource(url) else: if urltools.isRelative(name): # check for relative path defaultPath = globalref.mainControl.defaultPathObj(True) name = urltools.toAbsolute(name, str(defaultPath)) dataeditors.openExtUrl(name) def hasSelectedText(self): """Return True if text is selected. """ return self.textCursor().hasSelection() def highlightSearch(self, wordList=None, regExpList=None): """Highlight any found search terms. Arguments: wordList -- list of words to highlight regExpList -- a list of regular expression objects to highlight """ backColor = self.palette().brush(QPalette.Active, QPalette.Highlight) foreColor = self.palette().brush(QPalette.Active, QPalette.HighlightedText) if wordList is None: wordList = [] if regExpList is None: regExpList = [] for regExp in regExpList: for match in regExp.finditer(self.toPlainText()): matchText = match.group() if matchText not in wordList: wordList.append(matchText) selections = [] for word in wordList: while self.find(word): extraSel = QTextEdit.ExtraSelection() extraSel.cursor = self.textCursor() extraSel.format.setBackground(backColor) extraSel.format.setForeground(foreColor) selections.append(extraSel) cursor = QTextCursor(self.document()) self.setTextCursor(cursor) # reset main cursor/selection self.setExtraSelections(selections) def contextMenuEvent(self, event): """Add a popup menu for select all and copy actions. Arguments: event -- the menu event """ menu = self.createStandardContextMenu() menu.removeAction(menu.actions()[1]) #remove copy link location menu.exec_(event.globalPos()) 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.updateContents() return super().resizeEvent(event) TreeLine/source/treespot.py0000644000175000017500000002041513363127527014775 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treespot.py, provides a class to store locations of tree node instances # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import sys import operator class TreeSpot: """Class to store location info for tree node instances. Used to generate breadcrumb navigation and interface with tree views. A spot without a parent spot is an imaginary root spot, wihout a real node. """ def __init__(self, nodeRef, parentSpot): """Initialize a tree spot. Arguments: nodeRef -- reference to the associated tree node parentSpot -- the parent TreeSpot object """ self.nodeRef = nodeRef self.parentSpot = parentSpot def index(self, modelRef): """Returns the index of this spot in the tree model. Arguments: modelRef -- a ref to the tree model """ return modelRef.createIndex(self.row(), 0, self) def row(self): """Return the rank of this spot in its parent's child list. Should never be called from the imaginary root spot. """ try: return self.parentSpot.nodeRef.childList.index(self.nodeRef) except ValueError: return 0 # avoid error message from interim view updates def instanceNumber(self): """Return this spot's rank in the node's spot list. """ spotList = sorted(list(self.nodeRef.spotRefs), key=operator.methodcaller('sortKey')) return spotList.index(self) def spotId(self): """Return a spot ID string, in the form "nodeID:spotInstance". """ return '{0}:{1:d}'.format(self.nodeRef.uId, self.instanceNumber()) def isValid(self): """Return True if spot references and all parents are valid. """ spot = self while spot.parentSpot: if not (spot in spot.nodeRef.spotRefs and spot.nodeRef in spot.parentSpot.nodeRef.childList): return False spot = spot.parentSpot if not spot in spot.nodeRef.spotRefs: return False return True def spotDescendantGen(self): """Return a generator to step through all spots in this branch. Includes self. """ yield self for childSpot in self.childSpots(): for spot in childSpot.spotDescendantGen(): yield spot def spotDescendantOnlyGen(self): """Return a generator to step through the spots in this branch. Does not include self. """ for childSpot in self.childSpots(): yield childSpot for spot in childSpot.spotDescendantGen(): yield spot def expandedSpotDescendantGen(self, treeView): """Return a generator to step through expanded spots in this branch. Does not include root spot. Arguments: treeView -- a ref to the treeview """ for childSpot in self.childSpots(): if treeView.isSpotExpanded(childSpot): yield childSpot for spot in childSpot.expandedSpotDescendantGen(treeView): yield spot def levelSpotDescendantGen(self, treeView, includeRoot=True, maxLevel=None, openOnly=False, initLevel=0): """Return generator with (spot, level) tuples for this branch. Arguments: treeView -- a ref to the treeview, requiired to check if open includeRoot -- if True, the root spot is included maxLevel -- the max number of levels to return (no limit if none) openOnly -- if True, only include children open in the given view initLevel -- the level number to start with """ if maxLevel == None: maxLevel = sys.maxsize if includeRoot: yield (self, initLevel) initLevel += 1 if initLevel < maxLevel and (not openOnly or treeView.isSpotExpanded(self)): for childSpot in self.childSpots(): for spot, level in childSpot.levelSpotDescendantGen(treeView, True, maxLevel, openOnly, initLevel): yield (spot, level) def childSpots(self): """Return a list of immediate child spots. """ return [childNode.matchedSpot(self) for childNode in self.nodeRef.childList] def prevSiblingSpot(self): """Return the nearest previous sibling spot or None. """ if self.parentSpot: pos = self.row() if pos > 0: node = self.parentSpot.nodeRef.childList[pos - 1] return node.matchedSpot(self.parentSpot) return None def nextSiblingSpot(self): """Return the nearest next sibling spot or None. """ if self.parentSpot: childList = self.parentSpot.nodeRef.childList pos = self.row() + 1 if pos < len(childList): return childList[pos].matchedSpot(self.parentSpot) return None def prevTreeSpot(self, loop=False): """Return the previous node in the tree order. Return None at the start of the tree unless loop is true. Arguments: loop -- return the last node of the tree after the first if true """ sibling = self.prevSiblingSpot() if sibling: return sibling.lastDescendantSpot() if self.parentSpot.parentSpot: return self.parentSpot elif loop: return self.rootSpot().lastDescendantSpot() return None def nextTreeSpot(self, loop=False): """Return the next node in the tree order. Return None at the end of the tree unless loop is true. Arguments: loop -- return the root node at the end of the tree if true """ if self.nodeRef.childList: return self.nodeRef.childList[0].matchedSpot(self) ancestor = self while ancestor.parentSpot: sibling = ancestor.nextSiblingSpot() if sibling: return sibling ancestor = ancestor.parentSpot if loop: return ancestor.nodeRef.childList[0].matchedSpot(ancestor) return None def lastDescendantSpot(self): """Return the last spot of this spots's branch (last in tree order). """ spot = self while spot.nodeRef.childList: spot = spot.nodeRef.childList[-1].matchedSpot(spot) return spot def spotChain(self): """Return a list of parent spots, including self. """ chain = [] spot = self while spot.parentSpot: chain.insert(0, spot) spot = spot.parentSpot return chain def parentSpotSet(self): """Return a set of ancestor spots, not including self. """ result = set() spot = self.parentSpot while spot.parentSpot: result.add(spot) spot = spot.parentSpot return result def rootSpot(self): """Return the root spot that references the tree structure. """ spot = self while spot.parentSpot: spot = spot.parentSpot return spot def sortKey(self): """Return a tuple of parent row positions for sorting in tree order. """ positions = [] spot = self while spot.parentSpot: positions.insert(0, spot.row()) spot = spot.parentSpot return tuple(positions) TreeLine/source/conditional.py0000644000175000017500000006477213363127527015451 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # conditional.py, provides a class to store field comparison functions # # TreeLine, an information storage program # Copyright (C) 2017, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import enum from PyQt5.QtCore import QSize, Qt, pyqtSignal from PyQt5.QtWidgets import (QComboBox, QDialog, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QPushButton, QSizePolicy, QVBoxLayout) import treeformats import configdialog import undo import globalref _operators = ['==', '<', '<=', '>', '>=', '!=', N_('starts with'), N_('ends with'), N_('contains'), N_('True'), N_('False')] _functions = {'==': '__eq__', '<': '__lt__', '<=': '__le__', '>': '__gt__', '>=': '__ge__', '!=': '__ne__', 'starts with': 'startswith', 'ends with': 'endswith', 'contains': 'contains', 'True': 'true', 'False': 'false'} _boolOper = [N_('and'), N_('or')] _allTypeEntry = _('[All Types]') _parseRe = re.compile(r'((?:and)|(?:or)) (\S+) (.+?) ' r'(?:(? "othervalue"' Arguments: conditionStr -- the condition string to set nodeFormatName -- if name is set, restricts matches to type family """ self.conditionLines = [] conditionStr = 'and ' + conditionStr for boolOper, fieldName, oper, value in _parseRe.findall(conditionStr): value = value.replace('\\"', '"').replace('\\\\', '\\') self.conditionLines.append(ConditionLine(boolOper, fieldName, oper, value)) self.origNodeFormatName = nodeFormatName self.nodeFormatNames = set() if nodeFormatName: self.nodeFormatNames.add(nodeFormatName) nodeFormats = (globalref.mainControl.activeControl.structure. treeFormats) for nodeType in nodeFormats[nodeFormatName].derivedTypes: self.nodeFormatNames.add(nodeType.name) def evaluate(self, node): """Evaluate this condition and return True or False. Arguments: node -- the node to check for a field match """ if (self.nodeFormatNames and node.formatRef.name not in self.nodeFormatNames): return False result = True for conditon in self.conditionLines: result = conditon.evaluate(node, result) return result def conditionStr(self): """Return the condition string for this condition set. """ return ' '.join([cond.conditionStr() for cond in self.conditionLines])[4:] def renameFields(self, oldName, newName): """Rename the any fields found in condition lines. Arguments: oldName -- the previous field name newName -- the updated field name """ for condition in self.conditionLines: if condition.fieldName == oldName: condition.fieldName = newName def removeField(self, fieldname): """Remove conditional lines referencing the given field. Arguments: fieldname -- the field name to be removed """ for condition in self.conditionLines[:]: if condition.fieldName == fieldname: self.conditionLines.remove(condition) def __len__(self): """Return the number of conditions for truth testing. """ return len(self.conditionLines) class ConditionLine: """Stores & evaluates a portion of a conditional comparison. """ def __init__(self, boolOper, fieldName, oper, value): """Initialize the condition line. Arguments: boolOper -- a string for combining previous lines ('and' or 'or') fieldName -- the field name to evaluate oper -- the operator string value -- the string for comparison """ self.boolOper = boolOper self.fieldName = fieldName self.oper = oper self.value = value def evaluate(self, node, prevResult=True): """Evaluate this line and return True or False. Arguments: node -- the node to check for a field match prevResult -- the result to combine with the boolOper """ try: field = node.formatRef.fieldDict[self.fieldName] except KeyError: if self.boolOper == 'and': return False return prevResult dataStr = field.compareValue(node) value = field.adjustedCompareValue(self.value) try: func = getattr(dataStr, _functions[self.oper]) except AttributeError: dataStr = StringOps(dataStr) func = getattr(dataStr, _functions[self.oper]) value = str(value) if self.boolOper == 'and': return prevResult and func(value) else: return prevResult or func(value) def conditionStr(self): """Return the text line for this condition. """ value = self.value.replace('\\', '\\\\').replace('"', '\\"') return '{0} {1} {2} "{3}"'.format(self.boolOper, self.fieldName, self.oper, value) class StringOps(str): """A string class with extra comparison functions. """ def __new__(cls, initStr=''): """Return the str object. Arguments: initStr -- the initial string value """ return str.__new__(cls, initStr) def contains(self, substr): """Return True if self contains substr. Arguments: substr -- the substring to check """ return self.find(substr) != -1 def true(self, other=''): """Always return True. Arguments: other -- unused placeholder """ return True def false(self, other=''): """Always return False. Arguments: other -- unused placeholder """ return False FindDialogType = enum.Enum('FindDialogType', 'typeDialog findDialog filterDialog') class ConditionDialog(QDialog): """Dialog for defining field condition tests. Used for defining conditional types (modal), for finding by condition (nonmodal) and for filtering by condition (nonmodal). """ dialogShown = pyqtSignal(bool) def __init__(self, dialogType, caption, nodeFormat=None, parent=None): """Create the conditional dialog. Arguments: dialogType -- either typeDialog, findDialog or filterDialog caption -- the window title for this dialog nodeFormat -- the current node format for the typeDialog parent -- the parent overall dialog """ super().__init__(parent) self.setWindowTitle(caption) self.dialogType = dialogType self.ruleList = [] self.combiningBoxes = [] self.typeCombo = None self.resultLabel = None self.endFilterButton = None self.fieldNames = [] if nodeFormat: self.fieldNames = nodeFormat.fieldNames() topLayout = QVBoxLayout(self) if dialogType == FindDialogType.typeDialog: self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) else: self.setAttribute(Qt.WA_QuitOnClose, False) self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) typeBox = QGroupBox(_('Node Type')) topLayout.addWidget(typeBox) typeLayout = QVBoxLayout(typeBox) self.typeCombo = QComboBox() typeLayout.addWidget(self.typeCombo) self.typeCombo.currentIndexChanged.connect(self.updateDataType) self.mainLayout = QVBoxLayout() topLayout.addLayout(self.mainLayout) upCtrlLayout = QHBoxLayout() topLayout.addLayout(upCtrlLayout) addButton = QPushButton(_('&Add New Rule')) upCtrlLayout.addWidget(addButton) addButton.clicked.connect(self.addNewRule) self.removeButton = QPushButton(_('&Remove Rule')) upCtrlLayout.addWidget(self.removeButton) self.removeButton.clicked.connect(self.removeRule) upCtrlLayout.addStretch() if dialogType == FindDialogType.typeDialog: okButton = QPushButton(_('&OK')) upCtrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) upCtrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) else: self.removeButton.setEnabled(False) saveBox = QGroupBox(_('Saved Rules')) topLayout.addWidget(saveBox) saveLayout = QVBoxLayout(saveBox) self.saveListBox = SmallListWidget() saveLayout.addWidget(self.saveListBox) self.saveListBox.itemDoubleClicked.connect(self.loadSavedRule) nameLayout = QHBoxLayout() saveLayout.addLayout(nameLayout) label = QLabel(_('Name:')) nameLayout.addWidget(label) self.saveNameEdit = QLineEdit() nameLayout.addWidget(self.saveNameEdit) self.saveNameEdit.textChanged.connect(self.updateSaveEnable) saveButtonLayout = QHBoxLayout() saveLayout.addLayout(saveButtonLayout) self.loadSavedButton = QPushButton(_('&Load')) saveButtonLayout.addWidget(self.loadSavedButton) self.loadSavedButton.clicked.connect(self.loadSavedRule) self.saveButton = QPushButton(_('&Save')) saveButtonLayout.addWidget(self.saveButton) self.saveButton.clicked.connect(self.saveRule) self.saveButton.setEnabled(False) self.delSavedButton = QPushButton(_('&Delete')) saveButtonLayout.addWidget(self.delSavedButton) self.delSavedButton.clicked.connect(self.deleteRule) saveButtonLayout.addStretch() if dialogType == FindDialogType.findDialog: self.resultLabel = QLabel() topLayout.addWidget(self.resultLabel) lowCtrlLayout = QHBoxLayout() topLayout.addLayout(lowCtrlLayout) if dialogType == FindDialogType.findDialog: previousButton = QPushButton(_('Find &Previous')) lowCtrlLayout.addWidget(previousButton) previousButton.clicked.connect(self.findPrevious) nextButton = QPushButton(_('Find &Next')) nextButton.setDefault(True) lowCtrlLayout.addWidget(nextButton) nextButton.clicked.connect(self.findNext) else: filterButton = QPushButton(_('&Filter')) lowCtrlLayout.addWidget(filterButton) filterButton.clicked.connect(self.startFilter) self.endFilterButton = QPushButton(_('&End Filter')) lowCtrlLayout.addWidget(self.endFilterButton) self.endFilterButton.setEnabled(False) self.endFilterButton.clicked.connect(self.endFilter) lowCtrlLayout.addStretch() closeButton = QPushButton(_('&Close')) lowCtrlLayout.addWidget(closeButton) closeButton.clicked.connect(self.close) origTypeName = nodeFormat.name if nodeFormat else '' self.loadTypeNames(origTypeName) self.loadSavedNames() self.ruleList.append(ConditionRule(1, self.fieldNames)) self.mainLayout.addWidget(self.ruleList[0]) def addNewRule(self, checked=False, combineBool='and'): """Add a new empty rule to the dialog. Arguments: checked -- unused placekeeper variable for signal combineBool -- the boolean op for combining with the previous rule """ if self.ruleList: boolBox = QComboBox() boolBox.setEditable(False) self.combiningBoxes.append(boolBox) boolBox.addItems([_(op) for op in _boolOper]) if combineBool != 'and': boolBox.setCurrentIndex(1) self.mainLayout.insertWidget(len(self.ruleList) * 2 - 1, boolBox, 0, Qt.AlignHCenter) rule = ConditionRule(len(self.ruleList) + 1, self.fieldNames) self.ruleList.append(rule) self.mainLayout.insertWidget(len(self.ruleList) * 2 - 2, rule) self.removeButton.setEnabled(True) def removeRule(self): """Remove the last rule from the dialog. """ if self.ruleList: if self.combiningBoxes: self.combiningBoxes[-1].hide() del self.combiningBoxes[-1] self.ruleList[-1].hide() del self.ruleList[-1] if self.dialogType == FindDialogType.typeDialog: self.removeButton.setEnabled(len(self.ruleList) > 0) else: self.removeButton.setEnabled(len(self.ruleList) > 1) def clearRules(self): """Remove all rules from the dialog and add default rule. """ for box in self.combiningBoxes: box.hide() for rule in self.ruleList: rule.hide() self.combiningBoxes = [] self.ruleList = [ConditionRule(1, self.fieldNames)] self.mainLayout.insertWidget(0, self.ruleList[0]) self.removeButton.setEnabled(True) def setCondition(self, conditional, typeName=''): """Set rule values to match the given conditional. Arguments: conditional -- the Conditional class to match typeName -- an optional type name used with some dialog types """ if self.typeCombo: if typeName: self.typeCombo.setCurrentIndex(self.typeCombo. findText(typeName)) else: self.typeCombo.setCurrentIndex(0) while len(self.ruleList) > 1: self.removeRule() if conditional: self.ruleList[0].setCondition(conditional.conditionLines[0]) for conditionLine in conditional.conditionLines[1:]: self.addNewRule(combineBool=conditionLine.boolOper) self.ruleList[-1].setCondition(conditionLine) def conditional(self): """Return a Conditional instance for the current settings. """ combineBools = [0] + [boolBox.currentIndex() for boolBox in self.combiningBoxes] typeName = self.typeCombo.currentText() if self.typeCombo else '' if typeName == _allTypeEntry: typeName = '' conditional = Conditional('', typeName) for boolIndex, rule in zip(combineBools, self.ruleList): condition = rule.conditionLine() if boolIndex != 0: condition.boolOper = 'or' conditional.conditionLines.append(condition) return conditional def loadTypeNames(self, origTypeName=''): """Load format type names into combo box. Arguments: origTypeName -- a starting type name if given """ if not origTypeName: origTypeName = self.typeCombo.currentText() nodeFormats = globalref.mainControl.activeControl.structure.treeFormats self.typeCombo.blockSignals(True) self.typeCombo.clear() self.typeCombo.addItem(_allTypeEntry) typeNames = nodeFormats.typeNames() self.typeCombo.addItems(typeNames) if origTypeName and origTypeName != _allTypeEntry: try: self.typeCombo.setCurrentIndex(typeNames.index(origTypeName) + 1) except ValueError: if self.endFilterButton and self.endFilterButton.isEnabled(): self.endFilter() self.clearRules() self.typeCombo.blockSignals(False) self.updateDataType() def updateDataType(self): """Update the node format based on a data type change. """ typeName = self.typeCombo.currentText() if not typeName: return nodeFormats = globalref.mainControl.activeControl.structure.treeFormats if typeName == _allTypeEntry: fieldNameSet = set() for typeFormat in nodeFormats.values(): fieldNameSet.update(typeFormat.fieldNames()) self.fieldNames = sorted(list(fieldNameSet)) else: self.fieldNames = nodeFormats[typeName].fieldNames() for rule in self.ruleList: currentField = rule.conditionLine().fieldName if currentField not in self.fieldNames: if self.endFilterButton and self.endFilterButton.isEnabled(): self.endFilter() self.clearRules() break rule.reloadFieldBox(self.fieldNames, currentField) def loadSavedNames(self, updateOtherDialog=False): """Refresh the list of saved rule names. """ selNum = 0 if self.saveListBox.count(): selNum = self.saveListBox.currentRow() self.saveListBox.clear() nodeFormats = globalref.mainControl.activeControl.structure.treeFormats savedRules = nodeFormats.savedConditions() ruleNames = sorted(list(savedRules.keys())) if ruleNames: self.saveListBox.addItems(ruleNames) if selNum >= len(ruleNames): selNum = len(ruleNames) - 1 self.saveListBox.setCurrentRow(selNum) self.loadSavedButton.setEnabled(len(ruleNames) > 0) self.delSavedButton.setEnabled(len(ruleNames) > 0) if updateOtherDialog: if (self != globalref.mainControl.findConditionDialog and globalref.mainControl.findConditionDialog and globalref.mainControl.findConditionDialog.isVisible()): globalref.mainControl.findConditionDialog.loadSavedNames() elif (self != globalref.mainControl.filterConditionDialog and globalref.mainControl.filterConditionDialog and globalref.mainControl.filterConditionDialog .isVisible()): globalref.mainControl.filterConditionDialog.loadSavedNames() def updateSaveEnable(self): """Set the save rule button enabled based on save name entry. """ self.saveButton.setEnabled(len(self.saveNameEdit.text())) def updateFilterControls(self): """Set filter button status based on active window changes. """ window = globalref.mainControl.activeControl.activeWindow if window.treeFilterView: filterView = window.treeFilterView conditional = filterView.conditionalFilter self.setCondition(conditional, conditional.origNodeFormatName) self.endFilterButton.setEnabled(True) else: self.endFilterButton.setEnabled(False) def loadSavedRule(self): """Load the current saved rule into the dialog. """ nodeFormats = globalref.mainControl.activeControl.structure.treeFormats savedRules = nodeFormats.savedConditions() ruleName = self.saveListBox.currentItem().text() conditional = savedRules[ruleName] self.setCondition(conditional, conditional.origNodeFormatName) def saveRule(self): """Save the current rule settings. """ name = self.saveNameEdit.text() self.saveNameEdit.setText('') treeStructure = globalref.mainControl.activeControl.structure undo.FormatUndo(treeStructure.undoList, treeStructure.treeFormats, treeformats.TreeFormats()) typeName = self.typeCombo.currentText() if typeName == _allTypeEntry: nodeFormat = treeStructure.treeFormats else: nodeFormat = treeStructure.treeFormats[typeName] nodeFormat.savedConditionText[name] = (self.conditional(). conditionStr()) self.loadSavedNames(True) self.saveListBox.setCurrentItem(self.saveListBox. findItems(name, Qt.MatchExactly)[0]) globalref.mainControl.activeControl.setModified() def deleteRule(self): """Remove the current saved rule. """ treeStructure = globalref.mainControl.activeControl.structure nodeFormats = treeStructure.treeFormats undo.FormatUndo(treeStructure.undoList, nodeFormats, treeformats.TreeFormats()) savedRules = nodeFormats.savedConditions() ruleName = self.saveListBox.currentItem().text() conditional = savedRules[ruleName] if conditional.origNodeFormatName: typeFormat = nodeFormats[conditional. origNodeFormatName] del typeFormat.savedConditionText[ruleName] else: del nodeFormats.savedConditionText[ruleName] self.loadSavedNames(True) globalref.mainControl.activeControl.setModified() def find(self, forward=True): """Find another match in the indicated direction. Arguments: forward -- next if True, previous if False """ self.resultLabel.setText('') conditional = self.conditional() control = globalref.mainControl.activeControl if not control.findNodesByCondition(conditional, forward): self.resultLabel.setText(_('No conditional matches were found')) def findPrevious(self): """Find the previous match. """ self.find(False) def findNext(self): """Find the next match. """ self.find(True) def startFilter(self): """Start filtering nodes. """ window = globalref.mainControl.activeControl.activeWindow filterView = window.filterView() filterView.conditionalFilter = self.conditional() filterView.updateContents() self.endFilterButton.setEnabled(True) def endFilter(self): """Stop filtering nodes. """ window = globalref.mainControl.activeControl.activeWindow window.removeFilterView() self.endFilterButton.setEnabled(False) def closeEvent(self, event): """Signal that the dialog is closing. Arguments: event -- the close event """ self.dialogShown.emit(False) class ConditionRule(QGroupBox): """Group boxes for conditional rules in the ConditionDialog. """ def __init__(self, num, fieldNames, parent=None): """Create the conditional rule group box. Arguments: num -- the sequence number for the title fieldNames -- a list of available field names parent -- the parent dialog """ super().__init__(parent) self.fieldNames = fieldNames self.setTitle(_('Rule {0}').format(num)) layout = QHBoxLayout(self) self.fieldBox = QComboBox() self.fieldBox.setEditable(False) self.fieldBox.addItems(fieldNames) layout.addWidget(self.fieldBox) self.operBox = QComboBox() self.operBox.setEditable(False) self.operBox.addItems([_(op) for op in _operators]) layout.addWidget(self.operBox) self.operBox.currentIndexChanged.connect(self.changeOper) self.editor = QLineEdit() layout.addWidget(self.editor) self.fieldBox.setFocus() def reloadFieldBox(self, fieldNames, currentField=''): """Load the field combo box with a new field list. Arguments: fieldNames -- list of field names to add currentField -- a field name to make current if given """ self.fieldNames = fieldNames self.fieldBox.clear() self.fieldBox.addItems(fieldNames) if currentField: fieldNum = fieldNames.index(currentField) self.fieldBox.setCurrentIndex(fieldNum) self.changeOper() def setCondition(self, conditionLine): """Set values to match the given condition. Arguments: conditionLine -- the ConditionLine to match """ fieldNum = self.fieldNames.index(conditionLine.fieldName) self.fieldBox.setCurrentIndex(fieldNum) operNum = _operators.index(conditionLine.oper) self.operBox.setCurrentIndex(operNum) self.editor.setText(conditionLine.value) def conditionLine(self): """Return a conditionLine for the current settings. """ operTransDict = dict([(_(name), name) for name in _operators]) oper = operTransDict[self.operBox.currentText()] return ConditionLine('and', self.fieldBox.currentText(), oper, self.editor.text()) def changeOper(self): """Set the field available based on an operator change. """ realOp = self.operBox.currentText() not in (_(op) for op in ('True', 'False')) self.editor.setEnabled(realOp) if (not realOp and self.parent().typeCombo.currentText() == _allTypeEntry): realOp = True self.fieldBox.setEnabled(realOp) class SmallListWidget(QListWidget): """ListWidget with a smaller size hint. """ def __init__(self, parent=None): """Initialize the widget. Arguments: parent -- the parent, if given """ super().__init__(parent) def sizeHint(self): """Return smaller height. """ if self.count(): rowHeight = self.sizeHintForRow(0) else: self.addItem('tmp') rowHeight = self.sizeHintForRow(0) self.takeItem(0) newHeight = rowHeight * 3 + self.frameWidth() * 2 return QSize(super().sizeHint().width(), newHeight) TreeLine/source/treeselection.py0000644000175000017500000002303213363127527015773 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treeselection.py, provides a class for the tree view's selection model # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import collections import json from PyQt5.QtCore import QItemSelectionModel, QMimeData from PyQt5.QtGui import QClipboard from PyQt5.QtWidgets import QApplication import treestructure import treespotlist import globalref _maxHistoryLength = 10 class TreeSelection(QItemSelectionModel): """Class override for the tree view's selection model. Provides methods for easier access to selected nodes. """ def __init__(self, model, parent=None): """Initialize the selection model. Arguments: model -- the model for view data parent -- the parent tree view """ super().__init__(model, parent) self.modelRef = model self.tempExpandedSpots = [] self.prevSpots = [] self.nextSpots = [] self.restoreFlag = False self.selectionChanged.connect(self.updateSelectLists) def selectedCount(self): """Return the number of selected spots. """ return len(self.selectedIndexes()) def selectedSpots(self): """Return a SpotList of selected spots, sorted in tree order. """ return treespotlist.TreeSpotList([index.internalPointer() for index in self.selectedIndexes()]) def selectedBranchSpots(self): """Return a SpotList of spots at the top of selected branches. Remvoves any duplicate spots that are already covered by the branches. """ spots = self.selectedSpots() spotSet = set(spots) return treespotlist.TreeSpotList([spot for spot in spots if spot.parentSpotSet(). isdisjoint(spotSet)]) def selectedNodes(self): """Return a list of the currently selected tree nodes. Removes any duplicate (cloned) nodes. """ tmpDict = collections.OrderedDict() for spot in self.selectedSpots(): node = spot.nodeRef tmpDict[node.uId] = node return list(tmpDict.values()) def selectedBranches(self): """Return a list of nodes at the top of selected branches. Remvoves any duplicates that are already covered by the branches. """ tmpDict = collections.OrderedDict() for spot in self.selectedBranchSpots(): node = spot.nodeRef tmpDict[node.uId] = node return list(tmpDict.values()) def currentSpot(self): """Return the current tree spot. Can raise AttributeError if no spot is current. """ return self.currentIndex().internalPointer() def currentNode(self): """Return the current tree node. Can raise AttributeError if no node is current. """ return self.currentSpot().nodeRef def selectSpots(self, spotList, signalUpdate=True, expandParents=False): """Clear the current selection and select the given spots. Arguments: spotList -- the spots to select signalUpdate -- if False, block normal select update signals expandParents -- open parent spots to make selection visible """ if expandParents: treeView = (globalref.mainControl.activeControl.activeWindow. treeView) for spot in self.tempExpandedSpots: treeView.collapseSpot(spot) self.tempExpandedSpots = [] for spot in spotList: parent = spot.parentSpot while parent.parentSpot: if not treeView.isSpotExpanded(parent): treeView.expandSpot(parent) self.tempExpandedSpots.append(parent) parent = parent.parentSpot if not signalUpdate: self.blockSignals(True) self.addToHistory(spotList) self.clear() if spotList: for spot in spotList: self.select(spot.index(self.modelRef), QItemSelectionModel.Select) self.setCurrentIndex(spotList[0].index(self.modelRef), QItemSelectionModel.Current) self.blockSignals(False) def selectNodeById(self, nodeId): """Select the first spot from the given node ID. Return True on success. Arguments: nodeId -- the ID of the node to select """ try: node = self.modelRef.treeStructure.nodeDict[nodeId] self.selectSpots([node.spotByNumber(0)], True, True) except KeyError: return False return True def setCurrentSpot(self, spot): """Set the current spot. Arguments: spot -- the spot to make current """ self.blockSignals(True) self.setCurrentIndex(spot.index(self.modelRef), QItemSelectionModel.Current) self.blockSignals(False) def copySelectedNodes(self): """Copy these node branches to the clipboard. """ nodes = self.selectedBranches() if not nodes: return clip = QApplication.clipboard() if clip.supportsSelection(): titleList = [] for node in nodes: titleList.extend(node.exportTitleText()) clip.setText('\n'.join(titleList), QClipboard.Selection) struct = treestructure.TreeStructure(topNodes=nodes, addSpots=False) generics = {formatRef.genericType for formatRef in struct.treeFormats.values() if formatRef.genericType} for generic in generics: genericRef = self.modelRef.treeStructure.treeFormats[generic] struct.treeFormats.addTypeIfMissing(genericRef) for formatRef in genericRef.derivedTypes: struct.treeFormats.addTypeIfMissing(formatRef) data = struct.fileData() dataStr = json.dumps(data, indent=0, sort_keys=True) mime = QMimeData() mime.setData('application/json', bytes(dataStr, encoding='utf-8')) clip.setMimeData(mime) def restorePrevSelect(self): """Go back to the most recent saved selection. """ self.validateHistory() if len(self.prevSpots) > 1: del self.prevSpots[-1] oldSelect = self.selectedSpots() if oldSelect and (not self.nextSpots or oldSelect != self.nextSpots[-1]): self.nextSpots.append(oldSelect) self.restoreFlag = True self.selectSpots(self.prevSpots[-1], expandParents=True) self.restoreFlag = False def restoreNextSelect(self): """Go forward to the most recent saved selection. """ self.validateHistory() if self.nextSpots: select = self.nextSpots.pop(-1) if select and (not self.prevSpots or select != self.prevSpots[-1]): self.prevSpots.append(select) self.restoreFlag = True self.selectSpots(select, expandParents=True) self.restoreFlag = False def addToHistory(self, spots): """Add given spots to previous select list. Arguments: spots -- a list of spots to be added """ if spots and not self.restoreFlag and (not self.prevSpots or spots != self.prevSpots[-1]): self.prevSpots.append(spots) if len(self.prevSpots) > _maxHistoryLength: del self.prevSpots[:2] self.nextSpots = [] def validateHistory(self): """Clear invalid items from history lists. """ for histList in (self.prevSpots, self.nextSpots): for spots in histList: spots[:] = [spot for spot in spots if spot.isValid()] histList[:] = [spots for spots in histList if spots] def updateSelectLists(self): """Update history after a selection change. """ self.addToHistory(self.selectedSpots()) def selectTitleMatch(self, searchText, forward=True, includeCurrent=False): """Select a node with a title matching the search text. Returns True if found, otherwise False. Arguments: searchText -- the text to look for forward -- next if True, previous if False includeCurrent -- look in current node if True """ searchText = searchText.lower() currentSpot = self.currentSpot() spot = currentSpot while True: if not includeCurrent: if forward: spot = spot.nextTreeSpot(True) else: spot = spot.prevTreeSpot(True) if spot is currentSpot: return False includeCurrent = False if searchText in spot.nodeRef.title().lower(): self.selectSpots([spot], True, True) return True TreeLine/source/urltools.py0000644000175000017500000001124113363127530015002 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # urltools.py, provides functions for parsing and modifying URLs. # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import sys import os.path _urlRegExp = re.compile(r'([a-z]{2,}://)?(?:/?([a-z]:))?(.*)', re.IGNORECASE) def splitUrl(url): """Return a tuple of scheme, drive letter and address. If any are not present, return empty strings. Arguments: url -- a string with the original URL """ if os.sep == '\\': url = url.replace('\\', '/') scheme, drive, address = _urlRegExp.match(url).groups('') scheme = scheme[:-3] if not scheme and url.startswith('mailto:'): scheme = 'mailto' drive = '' address = url[7:] return (scheme, drive, address) def extractScheme(url): """Return the scheme from this URL, or an empty string if none is given. Arguments: url -- a string with the original URL """ scheme, drive, address = splitUrl(url) return scheme def extractAddress(url): """Remove the scheme from this URL and return the address. Includes the drive letter if present. Arguments: url -- a string with the original URL """ scheme, drive, address = splitUrl(url) return drive + address def replaceScheme(scheme, url): """Replace any scheme in url with the given scheme and return. The scheme is not included with a relative file path. Arguments: scheme -- the new scheme to add url -- the address be modified """ oldScheme, drive, address = splitUrl(url) if drive: drive = '/' + drive elif scheme == 'file' and not address.startswith('/'): return address elif scheme == 'mailto': return '{0}:{1}'.format(scheme, address) return '{0}://{1}{2}'.format(scheme, drive, address) def shortName(url): """Return a default short name using the base portion of the URL filename. Arguments: url -- a string with the original URL """ scheme, drive, address = splitUrl(url) name = os.path.basename(address) if not name: # remove trailing separator if there is no basename name = os.path.basename(address[:-1]) if scheme == 'mailto' or '@' in name: name = name.split('@', 1)[0] return name def isRelative(url): """Return true if this URL is a relative path. Any scheme or drive letter is considered absolute and returns false. Arguments: url -- a string with the original URL """ scheme, drive, address = splitUrl(url) if scheme or drive or address.startswith('/'): return False return True def toAbsolute(url, refPath, addScheme=True): """Convert a relative file URL to an absolute URL and return it. Arguments: url -- a string with the original URL refPath -- the path that the URL is relative to addScheme -- add the 'file' scheme to result if true """ scheme, drive, address = splitUrl(url) url = os.path.normpath(os.path.join(refPath, drive + address)) if addScheme: return replaceScheme('file', url) if os.sep == '\\': url = url.replace('\\', '/') return url def toRelative(url, refPath): """Convert an absolute file URL to a relative URL and return it. Arguments: url -- a string with the original URL refPath -- the path that the URL is relative to """ scheme, drive, address = splitUrl(url) if drive or address.startswith('/'): try: url = os.path.relpath(drive + address, refPath) except ValueError: pass if os.sep == '\\': url = url.replace('\\', '/') return url def which(fileName): """Return the full path if the fileName is found somewhere in the PATH. If not found, return an empty string. Similar to the Linux which command. Arguments: fileName -- the name to search for """ extList = [''] if sys.platform.startswith('win'): extList.extend(os.getenv('PATHEXT', '').split(os.pathsep)) for path in os.get_exec_path(): for ext in extList: fullPath = os.path.join(path, fileName + ext) if os.access(fullPath, os.X_OK): return fullPath return '' TreeLine/source/treemaincontrol.py0000644000175000017500000014040713760047721016337 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treemaincontrol.py, provides a class for global tree commands # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import sys import pathlib import os.path import ast import io import gzip import zlib import platform from PyQt5.QtCore import QIODevice, QObject, Qt, PYQT_VERSION_STR, qVersion from PyQt5.QtGui import QColor, QFont, QPalette from PyQt5.QtNetwork import QLocalServer, QLocalSocket from PyQt5.QtWidgets import (QAction, QApplication, QDialog, QFileDialog, QMessageBox, QStyleFactory, QSystemTrayIcon, qApp) import globalref import treelocalcontrol import options import optiondefaults import recentfiles import p3 import icondict import imports import configdialog import miscdialogs import conditional import colorset import helpview try: from __main__ import __version__, __author__ except ImportError: __version__ = '' __author__ = '' try: from __main__ import docPath, iconPath, templatePath, samplePath except ImportError: docPath = None iconPath = None templatePath = None samplePath = None encryptPrefix = b'>>TL+enc' class TreeMainControl(QObject): """Class to handle all global controls. Provides methods for all controls and stores local control objects. """ def __init__(self, pathObjects, parent=None): """Initialize the main tree controls Arguments: pathObjects -- a list of file objects to open parent -- the parent QObject if given """ super().__init__(parent) self.localControls = [] self.activeControl = None self.trayIcon = None self.isTrayMinimized = False self.configDialog = None self.sortDialog = None self.numberingDialog = None self.findTextDialog = None self.findConditionDialog = None self.findReplaceDialog = None self.filterTextDialog = None self.filterConditionDialog = None self.basicHelpView = None self.passwords = {} self.creatingLocalControlFlag = False globalref.mainControl = self self.allActions = {} try: # check for existing TreeLine session socket = QLocalSocket() socket.connectToServer('treeline3-session', QIODevice.WriteOnly) # if found, send files to open and exit TreeLine if socket.waitForConnected(1000): socket.write(bytes(repr([str(path) for path in pathObjects]), 'utf-8')) if socket.waitForBytesWritten(1000): socket.close() sys.exit(0) # start local server to listen for attempt to start new session self.serverSocket = QLocalServer() # remove any old servers still around after a crash in linux self.serverSocket.removeServer('treeline3-session') self.serverSocket.listen('treeline3-session') self.serverSocket.newConnection.connect(self.getSocket) except AttributeError: print(_('Warning: Could not create local socket')) mainVersion = '.'.join(__version__.split('.')[:2]) globalref.genOptions = options.Options('general', 'TreeLine', mainVersion, 'bellz') optiondefaults.setGenOptionDefaults(globalref.genOptions) globalref.miscOptions = options.Options('misc') optiondefaults.setMiscOptionDefaults(globalref.miscOptions) globalref.histOptions = options.Options('history') optiondefaults.setHistOptionDefaults(globalref.histOptions) globalref.toolbarOptions = options.Options('toolbar') optiondefaults.setToolbarOptionDefaults(globalref.toolbarOptions) globalref.keyboardOptions = options.Options('keyboard') optiondefaults.setKeyboardOptionDefaults(globalref.keyboardOptions) try: globalref.genOptions.readFile() globalref.miscOptions.readFile() globalref.histOptions.readFile() globalref.toolbarOptions.readFile() globalref.keyboardOptions.readFile() except IOError: errorDir = options.Options.basePath if not errorDir: errorDir = _('missing directory') QMessageBox.warning(None, 'TreeLine', _('Error - could not write config file to {}'). format(errorDir)) options.Options.basePath = None iconPathList = self.findResourcePaths('icons', iconPath) globalref.toolIcons = icondict.IconDict([path / 'toolbar' for path in iconPathList], ['', '32x32', '16x16']) globalref.toolIcons.loadAllIcons() windowIcon = globalref.toolIcons.getIcon('treelogo') if windowIcon: QApplication.setWindowIcon(windowIcon) globalref.treeIcons = icondict.IconDict(iconPathList, ['', 'tree']) icon = globalref.treeIcons.getIcon('default') qApp.setStyle(QStyleFactory.create('Fusion')) self.colorSet = colorset.ColorSet() if globalref.miscOptions['ColorTheme'] != 'system': self.colorSet.setAppColors() self.recentFiles = recentfiles.RecentFileList() if globalref.genOptions['AutoFileOpen'] and not pathObjects: recentPath = self.recentFiles.firstPath() if recentPath: pathObjects = [recentPath] self.setupActions() self.systemFont = QApplication.font() self.updateAppFont() if globalref.genOptions['MinToSysTray']: self.createTrayIcon() qApp.focusChanged.connect(self.updateActionsAvail) if pathObjects: for pathObj in pathObjects: self.openFile(pathObj, True) else: self.createLocalControl() def getSocket(self): """Open a socket from an attempt to open a second Treeline instance. Opens the file (or raise and focus if open) in this instance. """ socket = self.serverSocket.nextPendingConnection() if socket and socket.waitForReadyRead(1000): data = str(socket.readAll(), 'utf-8') try: paths = ast.literal_eval(data) if paths: for path in paths: pathObj = pathlib.Path(path) if pathObj != self.activeControl.filePathObj: self.openFile(pathObj, True) else: self.activeControl.activeWindow.activateAndRaise() else: self.activeControl.activeWindow.activateAndRaise() except(SyntaxError, ValueError, TypeError, RuntimeError): pass def findResourcePaths(self, resourceName, preferredPath=''): """Return list of potential non-empty pathlib objects for the resource. List includes preferred, module and user option paths. Arguments: resourceName -- the typical name of the resource directory preferredPath -- add this as the second path if given """ # use abspath() - pathlib's resolve() can be buggy with network drives modPath = pathlib.Path(os.path.abspath(sys.path[0])) if modPath.is_file(): modPath = modPath.parent # for frozen binary pathList = [modPath / '..' / resourceName, modPath / resourceName] if options.Options.basePath: basePath = pathlib.Path(options.Options.basePath) pathList.insert(0, basePath / resourceName) if preferredPath: pathList.insert(1, pathlib.Path(preferredPath)) return [pathlib.Path(os.path.abspath(str(path))) for path in pathList if path.is_dir() and list(path.iterdir())] def findResourceFile(self, fileName, resourceName, preferredPath=''): """Return a path object for a resource file. Add a language code before the extension if it exists. Arguments: fileName -- the name of the file to find resourceName -- the typical name of the resource directory preferredPath -- search this path first if given """ fileList = [fileName] if globalref.lang and globalref.lang != 'C': fileList[0:0] = [fileName.replace('.', '_{0}.'. format(globalref.lang)), fileName.replace('.', '_{0}.'. format(globalref.lang[:2]))] for fileName in fileList: for path in self.findResourcePaths(resourceName, preferredPath): if (path / fileName).is_file(): return path / fileName return None def defaultPathObj(self, dirOnly=False): """Return a reasonable default file path object. Used for open, save-as, import and export. Arguments: dirOnly -- if True, do not include basename of file """ pathObj = None if self.activeControl: pathObj = self.activeControl.filePathObj if not pathObj: pathObj = self.recentFiles.firstDir() if not pathObj: pathObj = pathlib.Path.home() if dirOnly: pathObj = pathObj.parent return pathObj def openFile(self, pathObj, forceNewWindow=False, checkModified=False, importOnFail=True): """Open the file given by path if not already open. If already open in a different window, focus and raise the window. Arguments: pathObj -- the path object to read forceNewWindow -- if True, use a new window regardless of option checkModified -- if True & not new win, prompt if file modified importOnFail -- if True, prompts for import on non-TreeLine files """ match = [control for control in self.localControls if pathObj == control.filePathObj] if match and self.activeControl not in match: control = match[0] control.activeWindow.activateAndRaise() self.updateLocalControlRef(control) return if checkModified and not (forceNewWindow or globalref.genOptions['OpenNewWindow'] or self.activeControl.checkSaveChanges()): return if not self.checkAutoSave(pathObj): if not self.localControls: self.createLocalControl() return QApplication.setOverrideCursor(Qt.WaitCursor) try: self.createLocalControl(pathObj, None, forceNewWindow) self.recentFiles.addItem(pathObj) if not (globalref.genOptions['SaveTreeStates'] and self.recentFiles.retrieveTreeState(self.activeControl)): self.activeControl.expandRootNodes() self.activeControl.selectRootSpot() QApplication.restoreOverrideCursor() except IOError: QApplication.restoreOverrideCursor() QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - could not read file {0}'). format(pathObj)) self.recentFiles.removeItem(pathObj) except (ValueError, KeyError, TypeError): fileObj = pathObj.open('rb') fileObj, encrypted = self.decryptFile(fileObj) if not fileObj: if not self.localControls: self.createLocalControl() QApplication.restoreOverrideCursor() return fileObj, compressed = self.decompressFile(fileObj) if compressed or encrypted: try: textFileObj = io.TextIOWrapper(fileObj, encoding='utf-8') self.createLocalControl(textFileObj, None, forceNewWindow) fileObj.close() textFileObj.close() self.recentFiles.addItem(pathObj) if not (globalref.genOptions['SaveTreeStates'] and self.recentFiles.retrieveTreeState(self. activeControl)): self.activeControl.expandRootNodes() self.activeControl.selectRootSpot() self.activeControl.compressed = compressed self.activeControl.encrypted = encrypted QApplication.restoreOverrideCursor() return except (ValueError, KeyError, TypeError): pass fileObj.close() importControl = imports.ImportControl(pathObj) structure = importControl.importOldTreeLine() if structure: self.createLocalControl(pathObj, structure, forceNewWindow) self.activeControl.printData.readData(importControl. treeLineRootAttrib) self.recentFiles.addItem(pathObj) self.activeControl.expandRootNodes() self.activeControl.imported = True QApplication.restoreOverrideCursor() return QApplication.restoreOverrideCursor() if importOnFail: importControl = imports.ImportControl(pathObj) structure = importControl.interactiveImport(True) if structure: self.createLocalControl(pathObj, structure, forceNewWindow) self.activeControl.imported = True return else: QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - invalid TreeLine file {0}'). format(pathObj)) self.recentFiles.removeItem(pathObj) if not self.localControls: self.createLocalControl() def decryptFile(self, fileObj): """Check for encryption and decrypt the fileObj if needed. Return a tuple of the file object and True if it was encrypted. Return None for the file object if the user cancels. Arguments: fileObj -- the file object to check and decrypt """ if fileObj.read(len(encryptPrefix)) != encryptPrefix: fileObj.seek(0) return (fileObj, False) while True: pathObj = pathlib.Path(fileObj.name) password = self.passwords.get(pathObj, '') if not password: QApplication.restoreOverrideCursor() dialog = miscdialogs.PasswordDialog(False, pathObj.name, QApplication. activeWindow()) if dialog.exec_() != QDialog.Accepted: fileObj.close() return (None, True) QApplication.setOverrideCursor(Qt.WaitCursor) password = dialog.password if miscdialogs.PasswordDialog.remember: self.passwords[pathObj] = password try: text = p3.p3_decrypt(fileObj.read(), password.encode()) fileIO = io.BytesIO(text) fileIO.name = fileObj.name fileObj.close() return (fileIO, True) except p3.CryptError: try: del self.passwords[pathObj] except KeyError: pass def decompressFile(self, fileObj): """Check for compression and decompress the fileObj if needed. Return a tuple of the file object and True if it was compressed. Arguments: fileObj -- the file object to check and decompress """ prefix = fileObj.read(2) fileObj.seek(0) if prefix != b'\037\213': return (fileObj, False) try: newFileObj = gzip.GzipFile(fileobj=fileObj) except zlib.error: return (fileObj, False) newFileObj.name = fileObj.name return (newFileObj, True) def checkAutoSave(self, pathObj): """Check for presence of auto save file & prompt user. Return True if OK to contimue, False if aborting or already loaded. Arguments: pathObj -- the base path object to search for a backup """ if not globalref.genOptions['AutoSaveMinutes']: return True basePath = pathObj pathObj = pathlib.Path(str(pathObj) + '~') if not pathObj.is_file(): return True msgBox = QMessageBox(QMessageBox.Information, 'TreeLine', _('Backup file "{}" exists.\nA previous ' 'session may have crashed'). format(pathObj), QMessageBox.NoButton, QApplication.activeWindow()) restoreButton = msgBox.addButton(_('&Restore Backup'), QMessageBox.ApplyRole) deleteButton = msgBox.addButton(_('&Delete Backup'), QMessageBox.DestructiveRole) cancelButton = msgBox.addButton(_('&Cancel File Open'), QMessageBox.RejectRole) msgBox.exec_() if msgBox.clickedButton() == restoreButton: self.openFile(pathObj) if self.activeControl.filePathObj != pathObj: return False try: basePath.unlink() pathObj.rename(basePath) except OSError: QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - could not rename "{0}" to "{1}"'). format(pathObj, basePath)) return False self.activeControl.filePathObj = basePath self.activeControl.updateWindowCaptions() self.recentFiles.removeItem(pathObj) self.recentFiles.addItem(basePath) return False elif msgBox.clickedButton() == deleteButton: try: pathObj.unlink() except OSError: QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - could not remove backup file {}'). format(pathObj)) else: # cancel button return False return True def createLocalControl(self, pathObj=None, treeStruct=None, forceNewWindow=False): """Create a new local control object and add it to the list. Use an imported structure if given or open the file if path is given. Arguments: pathObj -- the path object or file object for the control to open treeStruct -- the imported structure to use forceNewWindow -- if True, use a new window regardless of option """ self.creatingLocalControlFlag = True localControl = treelocalcontrol.TreeLocalControl(self.allActions, pathObj, treeStruct, forceNewWindow) localControl.controlActivated.connect(self.updateLocalControlRef) localControl.controlClosed.connect(self.removeLocalControlRef) self.localControls.append(localControl) self.updateLocalControlRef(localControl) self.creatingLocalControlFlag = False localControl.updateRightViews() localControl.updateCommandsAvail() def updateLocalControlRef(self, localControl): """Set the given local control as active. Called by signal from a window becoming active. Also updates non-modal dialogs. Arguments: localControl -- the new active local control """ if localControl != self.activeControl: self.activeControl = localControl if self.configDialog and self.configDialog.isVisible(): self.configDialog.setRefs(self.activeControl) def removeLocalControlRef(self, localControl): """Remove ref to local control based on a closing signal. Also do application exit clean ups if last control closing. Arguments: localControl -- the local control that is closing """ try: self.localControls.remove(localControl) except ValueError: return # skip for unreporducible bug - odd race condition? if globalref.genOptions['SaveTreeStates']: self.recentFiles.saveTreeState(localControl) if not self.localControls and not self.creatingLocalControlFlag: if globalref.genOptions['SaveWindowGeom']: localControl.windowList[0].saveWindowGeom() else: localControl.windowList[0].resetWindowGeom() self.recentFiles.writeItems() localControl.windowList[0].saveToolbarPosition() globalref.histOptions.writeFile() if self.trayIcon: self.trayIcon.hide() # stop listening for session connections try: self.serverSocket.close() del self.serverSocket except AttributeError: pass if self.localControls: # make sure a window is active (may not be focused), to avoid # bugs due to a deleted current window newControl = self.localControls[0] newControl.setActiveWin(newControl.windowList[0]) localControl.deleteLater() def createTrayIcon(self): """Create a new system tray icon if not already created. """ if QSystemTrayIcon.isSystemTrayAvailable: if not self.trayIcon: self.trayIcon = QSystemTrayIcon(qApp.windowIcon(), qApp) self.trayIcon.activated.connect(self.toggleTrayShow) self.trayIcon.show() def trayMinimize(self): """Minimize to tray based on window minimize signal. """ if self.trayIcon and QSystemTrayIcon.isSystemTrayAvailable: # skip minimize to tray if not all windows minimized for control in self.localControls: for window in control.windowList: if not window.isMinimized(): return for control in self.localControls: for window in control.windowList: window.hide() self.isTrayMinimized = True def toggleTrayShow(self): """Toggle show and hide application based on system tray icon click. """ if self.isTrayMinimized: for control in self.localControls: for window in control.windowList: window.show() window.showNormal() self.activeControl.activeWindow.treeView.setFocus() else: for control in self.localControls: for window in control.windowList: window.hide() self.isTrayMinimized = not self.isTrayMinimized def updateConfigDialog(self): """Update the config dialog for changes if it exists. """ if self.configDialog: self.configDialog.reset() def currentStatusBar(self): """Return the status bar from the current main window. """ return self.activeControl.activeWindow.statusBar() def windowActions(self): """Return a list of window menu actions from each local control. """ actions = [] for control in self.localControls: actions.extend(control.windowActions(len(actions) + 1, control == self.activeControl)) return actions def updateActionsAvail(self, oldWidget, newWidget): """Update command availability based on focus changes. Arguments: oldWidget -- the previously focused widget newWidget -- the newly focused widget """ self.allActions['FormatSelectAll'].setEnabled(hasattr(newWidget, 'selectAll') and not hasattr(newWidget, 'editTriggers')) def setupActions(self): """Add the actions for contols at the global level. """ fileNewAct = QAction(_('&New...'), self, toolTip=_('New File'), statusTip=_('Start a new file')) fileNewAct.triggered.connect(self.fileNew) self.allActions['FileNew'] = fileNewAct fileOpenAct = QAction(_('&Open...'), self, toolTip=_('Open File'), statusTip=_('Open a file from disk')) fileOpenAct.triggered.connect(self.fileOpen) self.allActions['FileOpen'] = fileOpenAct fileSampleAct = QAction(_('Open Sa&mple...'), self, toolTip=_('Open Sample'), statusTip=_('Open a sample file')) fileSampleAct.triggered.connect(self.fileOpenSample) self.allActions['FileOpenSample'] = fileSampleAct fileImportAct = QAction(_('&Import...'), self, statusTip=_('Open a non-TreeLine file')) fileImportAct.triggered.connect(self.fileImport) self.allActions['FileImport'] = fileImportAct fileQuitAct = QAction(_('&Quit'), self, statusTip=_('Exit the application')) fileQuitAct.triggered.connect(self.fileQuit) self.allActions['FileQuit'] = fileQuitAct dataConfigAct = QAction(_('&Configure Data Types...'), self, statusTip=_('Modify data types, fields & output lines'), checkable=True) dataConfigAct.triggered.connect(self.dataConfigDialog) self.allActions['DataConfigType'] = dataConfigAct dataVisualConfigAct = QAction(_('Show C&onfiguration Structure...'), self, statusTip=_('Show read-only visualization of type structure')) dataVisualConfigAct.triggered.connect(self.dataVisualConfig) self.allActions['DataVisualConfig'] = dataVisualConfigAct dataSortAct = QAction(_('Sor&t Nodes...'), self, statusTip=_('Define node sort operations'), checkable=True) dataSortAct.triggered.connect(self.dataSortDialog) self.allActions['DataSortNodes'] = dataSortAct dataNumberingAct = QAction(_('Update &Numbering...'), self, statusTip=_('Update node numbering fields'), checkable=True) dataNumberingAct.triggered.connect(self.dataNumberingDialog) self.allActions['DataNumbering'] = dataNumberingAct toolsFindTextAct = QAction(_('&Find Text...'), self, statusTip=_('Find text in node titles & data'), checkable=True) toolsFindTextAct.triggered.connect(self.toolsFindTextDialog) self.allActions['ToolsFindText'] = toolsFindTextAct toolsFindConditionAct = QAction(_('&Conditional Find...'), self, statusTip=_('Use field conditions to find nodes'), checkable=True) toolsFindConditionAct.triggered.connect(self.toolsFindConditionDialog) self.allActions['ToolsFindCondition'] = toolsFindConditionAct toolsFindReplaceAct = QAction(_('Find and &Replace...'), self, statusTip=_('Replace text strings in node data'), checkable=True) toolsFindReplaceAct.triggered.connect(self.toolsFindReplaceDialog) self.allActions['ToolsFindReplace'] = toolsFindReplaceAct toolsFilterTextAct = QAction(_('&Text Filter...'), self, statusTip=_('Filter nodes to only show text matches'), checkable=True) toolsFilterTextAct.triggered.connect(self.toolsFilterTextDialog) self.allActions['ToolsFilterText'] = toolsFilterTextAct toolsFilterConditionAct = QAction(_('C&onditional Filter...'), self, statusTip=_('Use field conditions to filter nodes'), checkable=True) toolsFilterConditionAct.triggered.connect(self. toolsFilterConditionDialog) self.allActions['ToolsFilterCondition'] = toolsFilterConditionAct toolsGenOptionsAct = QAction(_('&General Options...'), self, statusTip=_('Set user preferences for all files')) toolsGenOptionsAct.triggered.connect(self.toolsGenOptions) self.allActions['ToolsGenOptions'] = toolsGenOptionsAct toolsShortcutAct = QAction(_('Set &Keyboard Shortcuts...'), self, statusTip=_('Customize keyboard commands')) toolsShortcutAct.triggered.connect(self.toolsCustomShortcuts) self.allActions['ToolsShortcuts'] = toolsShortcutAct toolsToolbarAct = QAction(_('C&ustomize Toolbars...'), self, statusTip=_('Customize toolbar buttons')) toolsToolbarAct.triggered.connect(self.toolsCustomToolbars) self.allActions['ToolsToolbars'] = toolsToolbarAct toolsFontsAct = QAction(_('Customize Fo&nts...'), self, statusTip=_('Customize fonts in various views')) toolsFontsAct.triggered.connect(self.toolsCustomFonts) self.allActions['ToolsFonts'] = toolsFontsAct toolsColorsAct = QAction(_('Custo&mize Colors...'), self, statusTip=_('Customize GUI colors and themes')) toolsColorsAct.triggered.connect(self.toolsCustomColors) self.allActions['ToolsColors'] = toolsColorsAct formatSelectAllAct = QAction(_('&Select All'), self, statusTip=_('Select all text in an editor')) formatSelectAllAct.setEnabled(False) formatSelectAllAct.triggered.connect(self.formatSelectAll) self.allActions['FormatSelectAll'] = formatSelectAllAct helpBasicAct = QAction(_('&Basic Usage...'), self, statusTip=_('Display basic usage instructions')) helpBasicAct.triggered.connect(self.helpViewBasic) self.allActions['HelpBasic'] = helpBasicAct helpFullAct = QAction(_('&Full Documentation...'), self, statusTip=_('Open a TreeLine file with full documentation')) helpFullAct.triggered.connect(self.helpViewFull) self.allActions['HelpFull'] = helpFullAct helpAboutAct = QAction(_('&About TreeLine...'), self, statusTip=_('Display version info about this program')) helpAboutAct.triggered.connect(self.helpAbout) self.allActions['HelpAbout'] = helpAboutAct for name, action in self.allActions.items(): icon = globalref.toolIcons.getIcon(name.lower()) if icon: action.setIcon(icon) key = globalref.keyboardOptions[name] if not key.isEmpty(): action.setShortcut(key) def fileNew(self): """Start a new blank file. """ if (globalref.genOptions['OpenNewWindow'] or self.activeControl.checkSaveChanges()): searchPaths = self.findResourcePaths('templates', templatePath) if searchPaths: dialog = miscdialogs.TemplateFileDialog(_('New File'), _('&Select Template'), searchPaths) if dialog.exec_() == QDialog.Accepted: self.createLocalControl(dialog.selectedPath()) self.activeControl.filePathObj = None self.activeControl.updateWindowCaptions() self.activeControl.expandRootNodes() else: self.createLocalControl() self.activeControl.selectRootSpot() def fileOpen(self): """Prompt for a filename and open it. """ if (globalref.genOptions['OpenNewWindow'] or self.activeControl.checkSaveChanges()): filters = ';;'.join((globalref.fileFilters['trlnopen'], globalref.fileFilters['all'])) fileName, selFilter = QFileDialog.getOpenFileName(QApplication. activeWindow(), _('TreeLine - Open File'), str(self.defaultPathObj(True)), filters) if fileName: self.openFile(pathlib.Path(fileName)) def fileOpenSample(self): """Open a sample file from the doc directories. """ if (globalref.genOptions['OpenNewWindow'] or self.activeControl.checkSaveChanges()): searchPaths = self.findResourcePaths('samples', samplePath) dialog = miscdialogs.TemplateFileDialog(_('Open Sample File'), _('&Select Sample'), searchPaths, False) if dialog.exec_() == QDialog.Accepted: self.createLocalControl(dialog.selectedPath()) name = dialog.selectedName() + '.trln' self.activeControl.filePathObj = pathlib.Path(name) self.activeControl.updateWindowCaptions() self.activeControl.expandRootNodes() self.activeControl.imported = True def fileImport(self): """Prompt for an import type, then a file to import. """ importControl = imports.ImportControl() structure = importControl.interactiveImport() if structure: self.createLocalControl(importControl.pathObj, structure) if importControl.treeLineRootAttrib: self.activeControl.printData.readData(importControl. treeLineRootAttrib) self.activeControl.imported = True def fileQuit(self): """Close all windows to exit the applications. """ for control in self.localControls[:]: control.closeWindows() def dataConfigDialog(self, show): """Show or hide the non-modal data config dialog. Arguments: show -- true if dialog should be shown, false to hide it """ if show: if not self.configDialog: self.configDialog = configdialog.ConfigDialog() dataConfigAct = self.allActions['DataConfigType'] self.configDialog.dialogShown.connect(dataConfigAct.setChecked) self.configDialog.setRefs(self.activeControl, True) self.configDialog.show() else: self.configDialog.close() def dataVisualConfig(self): """Show a TreeLine file to visualize the config structure. """ structure = (self.activeControl.structure.treeFormats. visualConfigStructure(str(self.activeControl. filePathObj))) self.createLocalControl(treeStruct=structure, forceNewWindow=True) self.activeControl.filePathObj = pathlib.Path('structure.trln') self.activeControl.updateWindowCaptions() self.activeControl.expandRootNodes() self.activeControl.imported = True win = self.activeControl.activeWindow win.rightTabs.setCurrentWidget(win.outputSplitter) def dataSortDialog(self, show): """Show or hide the non-modal data sort nodes dialog. Arguments: show -- true if dialog should be shown, false to hide it """ if show: if not self.sortDialog: self.sortDialog = miscdialogs.SortDialog() dataSortAct = self.allActions['DataSortNodes'] self.sortDialog.dialogShown.connect(dataSortAct.setChecked) self.sortDialog.show() else: self.sortDialog.close() def dataNumberingDialog(self, show): """Show or hide the non-modal update node numbering dialog. Arguments: show -- true if dialog should be shown, false to hide it """ if show: if not self.numberingDialog: self.numberingDialog = miscdialogs.NumberingDialog() dataNumberingAct = self.allActions['DataNumbering'] self.numberingDialog.dialogShown.connect(dataNumberingAct. setChecked) self.numberingDialog.show() if not self.numberingDialog.checkForNumberingFields(): self.numberingDialog.close() else: self.numberingDialog.close() def toolsFindTextDialog(self, show): """Show or hide the non-modal find text dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.findTextDialog: self.findTextDialog = miscdialogs.FindFilterDialog() toolsFindTextAct = self.allActions['ToolsFindText'] self.findTextDialog.dialogShown.connect(toolsFindTextAct. setChecked) self.findTextDialog.selectAllText() self.findTextDialog.show() else: self.findTextDialog.close() def toolsFindConditionDialog(self, show): """Show or hide the non-modal conditional find dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.findConditionDialog: dialogType = conditional.FindDialogType.findDialog self.findConditionDialog = (conditional. ConditionDialog(dialogType, _('Conditional Find'))) toolsFindConditionAct = self.allActions['ToolsFindCondition'] (self.findConditionDialog.dialogShown. connect(toolsFindConditionAct.setChecked)) else: self.findConditionDialog.loadTypeNames() self.findConditionDialog.show() else: self.findConditionDialog.close() def toolsFindReplaceDialog(self, show): """Show or hide the non-modal find and replace text dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.findReplaceDialog: self.findReplaceDialog = miscdialogs.FindReplaceDialog() toolsFindReplaceAct = self.allActions['ToolsFindReplace'] self.findReplaceDialog.dialogShown.connect(toolsFindReplaceAct. setChecked) else: self.findReplaceDialog.loadTypeNames() self.findReplaceDialog.show() else: self.findReplaceDialog.close() def toolsFilterTextDialog(self, show): """Show or hide the non-modal filter text dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.filterTextDialog: self.filterTextDialog = miscdialogs.FindFilterDialog(True) toolsFilterTextAct = self.allActions['ToolsFilterText'] self.filterTextDialog.dialogShown.connect(toolsFilterTextAct. setChecked) self.filterTextDialog.selectAllText() self.filterTextDialog.show() else: self.filterTextDialog.close() def toolsFilterConditionDialog(self, show): """Show or hide the non-modal conditional filter dialog. Arguments: show -- true if dialog should be shown """ if show: if not self.filterConditionDialog: dialogType = conditional.FindDialogType.filterDialog self.filterConditionDialog = (conditional. ConditionDialog(dialogType, _('Conditional Filter'))) toolsFilterConditionAct = (self. allActions['ToolsFilterCondition']) (self.filterConditionDialog.dialogShown. connect(toolsFilterConditionAct.setChecked)) else: self.filterConditionDialog.loadTypeNames() self.filterConditionDialog.show() else: self.filterConditionDialog.close() def toolsGenOptions(self): """Set general user preferences for all files. """ oldAutoSaveMinutes = globalref.genOptions['AutoSaveMinutes'] dialog = options.OptionDialog(globalref.genOptions, QApplication.activeWindow()) dialog.setWindowTitle(_('General Options')) if (dialog.exec_() == QDialog.Accepted and globalref.genOptions.modified): globalref.genOptions.writeFile() self.recentFiles.updateOptions() if globalref.genOptions['MinToSysTray']: self.createTrayIcon() elif self.trayIcon: self.trayIcon.hide() autoSaveMinutes = globalref.genOptions['AutoSaveMinutes'] for control in self.localControls: for window in control.windowList: window.updateWinGenOptions() control.structure.undoList.setNumLevels() control.updateAll(False) if autoSaveMinutes != oldAutoSaveMinutes: control.resetAutoSave() def toolsCustomShortcuts(self): """Show dialog to customize keyboard commands. """ actions = self.activeControl.activeWindow.allActions dialog = miscdialogs.CustomShortcutsDialog(actions, QApplication. activeWindow()) dialog.exec_() def toolsCustomToolbars(self): """Show dialog to customize toolbar buttons. """ actions = self.activeControl.activeWindow.allActions dialog = miscdialogs.CustomToolbarDialog(actions, self.updateToolbars, QApplication. activeWindow()) dialog.exec_() def updateToolbars(self): """Update toolbars after changes in custom toolbar dialog. """ for control in self.localControls: for window in control.windowList: window.setupToolbars() def toolsCustomFonts(self): """Show dialog to customize fonts in various views. """ dialog = miscdialogs.CustomFontDialog(QApplication. activeWindow()) dialog.updateRequired.connect(self.updateCustomFonts) dialog.exec_() def toolsCustomColors(self): """Show dialog to customize GUI colors ans themes. """ self.colorSet.showDialog(QApplication.activeWindow()) def updateCustomFonts(self): """Update fonts in all windows based on a dialog signal. """ self.updateAppFont() for control in self.localControls: for window in control.windowList: window.updateFonts() control.printData.setDefaultFont() for control in self.localControls: control.updateAll(False) def updateAppFont(self): """Update application default font from settings. """ appFont = QFont(self.systemFont) appFontName = globalref.miscOptions['AppFont'] if appFontName: appFont.fromString(appFontName) QApplication.setFont(appFont) def formatSelectAll(self): """Select all text in any currently focused editor. """ try: QApplication.focusWidget().selectAll() except AttributeError: pass def helpViewBasic(self): """Display basic usage instructions. """ if not self.basicHelpView: path = self.findResourceFile('basichelp.html', 'doc', docPath) if not path: QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - basic help file not found')) return self.basicHelpView = helpview.HelpView(path, _('TreeLine Basic Usage'), globalref.toolIcons) self.basicHelpView.show() def helpViewFull(self): """Open a TreeLine file with full documentation. """ path = self.findResourceFile('documentation.trln', 'doc', docPath) if not path: QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', _('Error - documentation file not found')) return self.createLocalControl(path, forceNewWindow=True) self.activeControl.filePathObj = pathlib.Path('documentation.trln') self.activeControl.updateWindowCaptions() self.activeControl.expandRootNodes() self.activeControl.imported = True win = self.activeControl.activeWindow win.rightTabs.setCurrentWidget(win.outputSplitter) def helpAbout(self): """ Display version info about this program. """ pyVersion = '.'.join([repr(num) for num in sys.version_info[:3]]) textLines = [_('TreeLine version {0}').format(__version__), _('written by {0}').format(__author__), '', _('Library versions:'), ' Python: {0}'.format(pyVersion), ' Qt: {0}'.format(qVersion()), ' PyQt: {0}'.format(PYQT_VERSION_STR), ' OS: {0}'.format(platform.platform())] dialog = miscdialogs.AboutDialog('TreeLine', textLines, QApplication.windowIcon(), QApplication.activeWindow()) dialog.exec_() def setThemeColors(): """Set the app colors based on options setting. """ if globalref.genOptions['ColorTheme'] == optiondefaults.colorThemes[1]: # dark theme myDarkGray = QColor(53, 53, 53) myVeryDarkGray = QColor(25, 25, 25) myBlue = QColor(42, 130, 218) palette = QPalette() palette.setColor(QPalette.Window, myDarkGray) palette.setColor(QPalette.WindowText, Qt.white) palette.setColor(QPalette.Base, myVeryDarkGray) palette.setColor(QPalette.AlternateBase, myDarkGray) palette.setColor(QPalette.ToolTipBase, Qt.darkBlue) palette.setColor(QPalette.ToolTipText, Qt.lightGray) palette.setColor(QPalette.Text, Qt.white) palette.setColor(QPalette.Button, myDarkGray) palette.setColor(QPalette.ButtonText, Qt.white) palette.setColor(QPalette.BrightText, Qt.red) palette.setColor(QPalette.Link, myBlue) palette.setColor(QPalette.Highlight, myBlue) palette.setColor(QPalette.HighlightedText, Qt.black) palette.setColor(QPalette.Disabled, QPalette.Text, Qt.darkGray) palette.setColor(QPalette.Disabled, QPalette.ButtonText, Qt.darkGray) qApp.setPalette(palette) TreeLine/source/treespotlist.py0000644000175000017500000003015313363127527015671 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treespotlist.py, provides a class to do operations on groups of spots # # TreeLine, an information storage program # Copyright (C) 2018, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import collections import operator from PyQt5.QtWidgets import QApplication import treestructure import undo class TreeSpotList(list): """Class to do operations on groups of spots. Stores a list of nodes. """ def __init__(self, spotList=None, sortSpots=True): """Initialize a tree spot group. Arguments: spotList -- the initial list of spots sortSpots -- if True sort the spots in tree order """ super().__init__() if spotList: self[:] = spotList if sortSpots: self.sort(key=operator.methodcaller('sortKey')) def relatedNodes(self): """Return a list of nodes related to these spots. Removes any duplicate (cloned) nodes. """ tmpDict = collections.OrderedDict() for spot in self: node = spot.nodeRef tmpDict[node.uId] = node return list(tmpDict.values()) def pasteChild(self, treeStruct, treeView): """Paste child nodes from the clipbaord. Return True on success. Arguments: treeStruct -- a ref to the existing tree structure treeView -- a ref to the tree view for expanding nodes """ mimeData = QApplication.clipboard().mimeData() parentNodes = self.relatedNodes() if not parentNodes: parentNodes = [treeStruct] undoObj = undo.ChildListUndo(treeStruct.undoList, parentNodes, treeFormats=treeStruct.treeFormats) for parent in parentNodes: newStruct = treestructure.structFromMimeData(mimeData) if not newStruct: treeStruct.undoList.removeLastUndo(undoObj) return False newStruct.replaceDuplicateIds(treeStruct.nodeDict) treeStruct.addNodesFromStruct(newStruct, parent) for spot in self: treeView.expandSpot(spot) return True def pasteSibling(self, treeStruct, insertBefore=True): """Paste a sibling at the these spots. Return True on success. Arguments: treeStruct -- a ref to the existing tree structure insertBefore -- if True, insert before these nodes, o/w after """ mimeData = QApplication.clipboard().mimeData() parentNodes = [spot.parentSpot.nodeRef for spot in self] undoObj = undo.ChildListUndo(treeStruct.undoList, parentNodes, treeFormats=treeStruct.treeFormats) for spot in self: newStruct = treestructure.structFromMimeData(mimeData) if not newStruct: treeStruct.undoList.removeLastUndo(undoObj) return False newStruct.replaceDuplicateIds(treeStruct.nodeDict) parent = spot.parentSpot.nodeRef pos = parent.childList.index(spot.nodeRef) if not insertBefore: pos += 1 treeStruct.addNodesFromStruct(newStruct, parent, pos) return True def pasteCloneChild(self, treeStruct, treeView): """Paste child clones from the clipbaord. Return True on success. Arguments: treeStruct -- a ref to the existing tree structure treeView -- a ref to the tree view for expanding nodes """ mimeData = QApplication.clipboard().mimeData() newStruct = treestructure.structFromMimeData(mimeData) if not newStruct: return False try: existNodes = [treeStruct.nodeDict[node.uId] for node in newStruct.childList] except KeyError: return False # nodes copied from other file parentNodes = self.relatedNodes() if not parentNodes: parentNodes = [treeStruct] for parent in parentNodes: if not parent.ancestors().isdisjoint(set(existNodes)): return False # circular ref for node in existNodes: if parent in node.parents(): return False # identical siblings undoObj = undo.ChildListUndo(treeStruct.undoList, parentNodes, treeFormats=treeStruct.treeFormats) for parent in parentNodes: for node in existNodes: parent.childList.append(node) node.addSpotRef(parent) for spot in self: treeView.expandSpot(spot) return True def pasteCloneSibling(self, treeStruct, insertBefore=True): """Paste sibling clones at the these spots. Return True on success. Arguments: treeStruct -- a ref to the existing tree structure insertBefore -- if True, insert before these nodes, o/w after """ mimeData = QApplication.clipboard().mimeData() newStruct = treestructure.structFromMimeData(mimeData) if not newStruct: return False try: existNodes = [treeStruct.nodeDict[node.uId] for node in newStruct.childList] except KeyError: return False # nodes copied from other file parentNodes = [spot.parentSpot.nodeRef for spot in self] for parent in parentNodes: if not parent.ancestors().isdisjoint(set(existNodes)): return False # circular ref for node in existNodes: if parent in node.parents(): return False # identical siblings undoObj = undo.ChildListUndo(treeStruct.undoList, parentNodes, treeFormats=treeStruct.treeFormats) for spot in self: parent = spot.parentSpot.nodeRef pos = parent.childList.index(spot.nodeRef) if not insertBefore: pos += 1 for node in existNodes: parent.childList.insert(pos, node) node.addSpotRef(parent) return True def addChild(self, treeStruct, treeView): """Add new child to these spots. Return the new spots. Arguments: treeStruct -- a ref to the existing tree structure treeView -- a ref to the tree view for expanding nodes """ selSpots = self if not selSpots: selSpots = list(treeStruct.spotRefs) undo.ChildListUndo(treeStruct.undoList, [spot.nodeRef for spot in selSpots]) newSpots = [] for spot in selSpots: newNode = spot.nodeRef.addNewChild(treeStruct) newSpots.append(newNode.matchedSpot(spot)) if spot.parentSpot: # can't expand root struct spot treeView.expandSpot(spot) return newSpots def insertSibling(self, treeStruct, insertBefore=True): """Insert a new sibling node at these nodes. Return the new spots. Arguments: treeStruct -- a ref to the existing tree structure insertBefore -- if True, insert before these nodes, o/w after """ undo.ChildListUndo(treeStruct.undoList, [spot.parentSpot.nodeRef for spot in self]) newSpots = [] for spot in self: newNode = spot.parentSpot.nodeRef.addNewChild(treeStruct, spot.nodeRef, insertBefore) newSpots.append(newNode.matchedSpot(spot.parentSpot)) return newSpots def delete(self, treeStruct): """Delete these spots, return a new spot to select. Arguments: treeStruct -- a ref to the existing tree structure """ # gather next selected node in decreasing order of desirability nextSel = [spot.nextSiblingSpot() for spot in self] nextSel.extend([spot.prevSiblingSpot() for spot in self]) nextSel.extend([spot.parentSpot for spot in self]) while (not nextSel[0] or not nextSel[0].parentSpot or nextSel[0] in self): del nextSel[0] spotSet = set(self) branchSpots = [spot for spot in self if spot.parentSpotSet().isdisjoint(spotSet)] undoParents = {spot.parentSpot.nodeRef for spot in branchSpots} undo.ChildListUndo(treeStruct.undoList, list(undoParents)) for spot in branchSpots: treeStruct.deleteNodeSpot(spot) return nextSel[0] def indent(self, treeStruct): """Indent these spots. Makes them children of their previous siblings. Return the new spots. Arguments: treeStruct -- a ref to the existing tree structure """ undoSpots = ([spot.parentSpot for spot in self] + [spot.prevSiblingSpot() for spot in self]) undo.ChildListUndo(treeStruct.undoList, [spot.nodeRef for spot in undoSpots]) newSpots = [] for spot in self: node = spot.nodeRef newParentSpot = spot.prevSiblingSpot() node.changeParent(spot.parentSpot, newParentSpot) newSpots.append(node.matchedSpot(newParentSpot)) return newSpots def unindent(self, treeStruct): """Unindent these spots. Makes them their parent's next sibling. Return the new spots. Arguments: treeStruct -- a ref to the existing tree structure """ undoSpots = [spot.parentSpot for spot in self] undoSpots.extend([spot.parentSpot for spot in undoSpots]) undo.ChildListUndo(treeStruct.undoList, [spot.nodeRef for spot in undoSpots]) newSpots = [] for spot in reversed(self): node = spot.nodeRef oldParentSpot = spot.parentSpot newParentSpot = oldParentSpot.parentSpot pos = (newParentSpot.nodeRef.childList.index(oldParentSpot.nodeRef) + 1) node.changeParent(oldParentSpot, newParentSpot, pos) newSpots.append(node.matchedSpot(newParentSpot)) return newSpots def move(self, treeStruct, up=True): """Move these spots up or down by one item. Arguments: treeStruct -- a ref to the existing tree structure up -- if True move up, o/w down """ undo.ChildListUndo(treeStruct.undoList, [spot.parentSpot.nodeRef for spot in self]) if not up: self.reverse() for spot in self: parent = spot.parentSpot.nodeRef pos = parent.childList.index(spot.nodeRef) del parent.childList[pos] pos = pos - 1 if up else pos + 1 parent.childList.insert(pos, spot.nodeRef) def moveToEnd(self, treeStruct, first=True): """Move these spots to the first or last position. Arguments: treeStruct -- a ref to the existing tree structure first -- if True move to first position, o/w last """ undo.ChildListUndo(treeStruct.undoList, [spot.parentSpot.nodeRef for spot in self]) if first: self.reverse() for spot in self: parent = spot.parentSpot.nodeRef parent.childList.remove(spot.nodeRef) if first: parent.childList.insert(0, spot.nodeRef) else: parent.childList.append(spot.nodeRef) TreeLine/source/treeline.pro0000644000175000017500000000272213262465526015112 0ustar dougdougSOURCES = breadcrumbview.py \ conditional.py \ configdialog.py \ dataeditors.py \ dataeditview.py \ exports.py \ fieldformat.py \ genboolean.py \ gennumber.py \ globalref.py \ helpview.py \ icondict.py \ imports.py \ matheval.py \ miscdialogs.py \ nodeformat.py \ numbering.py \ optiondefaults.py \ options.py \ outputview.py \ p3.py \ printdata.py \ printdialogs.py \ recentfiles.py \ spellcheck.py \ titlelistview.py \ treeformats.py \ treeline.py \ treelocalcontrol.py \ treemaincontrol.py \ treemodel.py \ treenode.py \ treeoutput.py \ treeselection.py \ treespotlist.py \ treespot.py \ treestructure.py \ treeview.py \ treewindow.py \ undo.py \ urltools.py TRANSLATIONS = treeline_de.ts \ treeline_es.ts \ treeline_fr.ts \ treeline_it.ts \ treeline_pt.ts \ treeline_ru.ts \ treeline_xx.ts TreeLine/source/spellcheck.py0000644000175000017500000005357413745103733015256 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # spellcheck.py, provides classes for spell check interfaces and dialogs, # including interfaces to aspell, ispell, hunspell. # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import re import sys import subprocess import collections from PyQt5.QtCore import QSize, Qt, pyqtSignal from PyQt5.QtGui import QFontMetrics, QTextCursor from PyQt5.QtWidgets import (QApplication, QDialog, QFileDialog, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QMessageBox, QPushButton, QTextEdit, QVBoxLayout) import undo import globalref _guessRe = re.compile(r'[&?] (\S+) \d+ (\d+): (.+)') _noGuessRe = re.compile(r'# (\S+) (\d+)') class SpellCheckInterface: """Interfaces with aspell, ispell or hunspell and stores session hooks. """ def __init__(self, spellPath='', langCode=''): """Create initial hooks to outside program. Arguments: spellPath -- use to find engine executable if given langCode -- language code to pass to aspell if given """ engineOptions = collections.OrderedDict() engineOptions.update([('aspell', ['-a -H --encoding=utf-8']), ('ispell', ['-a -h -Tutf8', '-a']), ('hunspell', ['-a -H -i utf-8'])]) langPrefix = {'aspell': 'l', 'ispell': 'd', 'hunspell': 'd'} if spellPath: newEngineOptions = {} for engine in engineOptions.keys(): if engine in spellPath: newEngineOptions[spellPath] = engineOptions[engine] engineOptions = newEngineOptions for engine, options in engineOptions.items(): if langCode: options.insert(0, '{0} -{1} {2}'.format(options[0], langPrefix[engine], langCode)) for option in options: cmd = '{0} {1}'.format(engine, option) try: p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.stdIn = p.stdin self.stdOut = p.stdout self.stdOut.readline() # read header # set terse mode (no correct returns) self.stdIn.write(b'!\n') self.stdIn.flush() return except IOError: pass raise SpellCheckError('Could not initialize aspell, ispell or ' 'hunspell') def checkLine(self, line, skipWords=None): """Check one (and only one) line of text. Return a list of tuples, each with the mispelled word, position in the line, and a list of suggestions. Arguments: line -- the text string to check skipWords -- a set of words to ignore if given """ if not skipWords: skipWords = set() self.stdIn.write('^{0}\n'.format(line).encode('utf-8')) self.stdIn.flush() outputs = [self.stdOut.readline()] while outputs[-1].strip(): outputs.append(self.stdOut.readline()) results = [] for output in outputs: output = output.decode('utf-8').strip() match = _guessRe.match(output) if match: guesses = match.group(3).split(', ') else: match = _noGuessRe.match(output) guesses = [] if match: word = match.group(1) if word not in skipWords: 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 results.append((word, wordPos, guesses)) return 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. Arguments: word -- the word to accept """ self.stdIn.write('@{0}\n'.format(word).encode('utf-8')) self.stdIn.flush() def addToDict(self, word, lowCase=False): """Add word to spell check engine's dictionary. Arguments: word -- the word to add lowCase -- if True, add the word as a lower case word """ if lowCase: self.stdIn.write('&{0}\n'.format(word).encode('utf-8')) else: self.stdIn.write('*{0}\n'.format(word).encode('utf-8')) self.stdIn.write(b'#\n') # saves dict self.stdIn.flush() class SpellCheckError(Exception): """Exception class for errors interfacing with the spell check engine. """ pass # console test for the spell check engine interface if __name__ == '__main__': try: sp = SpellCheckInterface() except SpellCheckError: print('Error - could not initialize aspell, ispell or hunspell') sys.exit() while True: s = 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:], True) else: for word, pos, suggests in sp.checkLine(s): print('{0} @{1}: {2}\n'.format(word, pos, ', '.join(suggests))) sp.close() class SpellCheckOperation: """Feeds tree node text to the spell check dialog. """ def __init__(self, controlRef): """Initialize the spell check engine interface. Arguments: controlRef - the local control """ self.controlRef = controlRef self.selectModel = controlRef.currentSelectionModel() self.currentSpot = None self.currentField = '' self.lineNum = 0 self.textLine = '' parentWidget = QApplication.activeWindow() path = globalref.miscOptions['SpellCheckPath'] while True: try: self.spellCheckInterface = SpellCheckInterface(path, self.controlRef. spellCheckLang) return except SpellCheckError: if path: path = '' else: if sys.platform.startswith('win'): prompt = (_('Could not find either aspell.exe, ' 'ispell.exe or hunspell.exe\n' 'Browse for location?')) ans = QMessageBox.warning(parentWidget, _('Spell Check Error'), prompt, QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Yes) if ans == QMessageBox.Cancel: raise title = _('Locate aspell.exe, ipsell.exe or ' 'hunspell.exe') path, fltr = QFileDialog.getOpenFileName(parentWidget, title, '', _('Program (*.exe)')) if path: path = path[:-4] if ' ' in path: path = '"{0}"'.format(path) globalref.miscOptions.changeValue('SpellCheckPath', path) globalref.miscOptions.writeFile() else: prompt = (_('TreeLine Spell Check Error\nMake sure ' 'aspell, ispell or hunspell is installed')) QMessageBox.warning(parentWidget, 'TreeLine', prompt) raise def spellCheck(self): """Spell check starting with the selected branches. """ parentWidget = QApplication.activeWindow() spellCheckDialog = SpellCheckDialog(self.spellCheckInterface, parentWidget) spellCheckDialog.misspellFound.connect(self.updateSelection) spellCheckDialog.changeRequest.connect(self.changeNode) origBranches = self.selectModel.selectedBranchSpots() if not origBranches: origBranches = self.controlRef.structure.rootSpots() result = (spellCheckDialog. startSpellCheck(self.textLineGenerator(origBranches))) self.selectModel.selectSpots(origBranches, expandParents = True) if result and origBranches[0].parentSpot.parentSpot: prompt = _('Finished checking the branch\nContinue from the top?') ans = QMessageBox.information(parentWidget, _('TreeLine Spell Check'), prompt, QMessageBox.Yes | QMessageBox.No) if ans == QMessageBox.Yes: generator = self.textLineGenerator(self.controlRef.structure. rootSpots()) result = spellCheckDialog.startSpellCheck(generator) self.selectModel.selectSpots(origBranches, expandParents = True) else: result = False if result: QMessageBox.information(parentWidget, _('TreeLine Spell Check'), _('Finished spell checking')) def updateSelection(self): """Change the tree selection to the node with a misspelled word. """ self.selectModel.selectSpots([self.currentSpot], expandParents = True) def changeNode(self, newTextLine): """Replace the current text line in the current node. Arguments: newTextLine -- the new text to use """ node = self.currentSpot.nodeRef undo.DataUndo(self.controlRef.structure.undoList, node) textLines = node.data.get(self.currentField, '').split('\n') textLines[self.lineNum] = newTextLine node.data[self.currentField] = '\n'.join(textLines) self.controlRef.updateTreeNode(node) def textLineGenerator(self, branches): """Yield next line to be checked. Arguments: branches -- a list of branch parent nodes to check. """ for parent in branches: for self.currentSpot in parent.spotDescendantGen(): node = self.currentSpot.nodeRef for self.currentField in node.formatRef.fieldNames(): text = node.data.get(self.currentField, '') if text: for self.lineNum, self.textLine in \ enumerate(text.split('\n')): yield self.textLine class SpellCheckDialog(QDialog): """Dialog to perform and control the spell check operation. """ misspellFound = pyqtSignal() changeRequest = pyqtSignal(str) def __init__(self, spellCheckInterface, parent=None): """Create the dialog. Arguments: spellCheckInterface -- a reference to the spell engine interface parent -- the parent dialog """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Spell Check')) self.spellCheckInterface = spellCheckInterface self.textLineIter = None self.textLine = '' self.replaceAllDict = {} self.tmpIgnoreWords = set() self.word = '' self.postion = 0 topLayout = QHBoxLayout(self) leftLayout = QVBoxLayout() topLayout.addLayout(leftLayout) wordBox = QGroupBox(_('Not in Dictionary')) leftLayout.addWidget(wordBox) wordLayout = QVBoxLayout(wordBox) label = QLabel(_('Word:')) wordLayout.addWidget(label) self.wordEdit = QLineEdit() wordLayout.addWidget(self.wordEdit) self.wordEdit.textChanged.connect(self.updateFromWord) wordLayout.addSpacing(5) label = QLabel(_('Context:')) wordLayout.addWidget(label) self.contextEdit = SpellContextEdit() wordLayout.addWidget(self.contextEdit) self.contextEdit.textChanged.connect(self.updateFromContext) suggestBox = QGroupBox(_('Suggestions')) leftLayout.addWidget(suggestBox) suggestLayout = QVBoxLayout(suggestBox) self.suggestList = QListWidget() suggestLayout.addWidget(self.suggestList) self.suggestList.itemDoubleClicked.connect(self.replace) rightLayout = QVBoxLayout() topLayout.addLayout(rightLayout) ignoreButton = QPushButton(_('Ignor&e')) rightLayout.addWidget(ignoreButton) ignoreButton.clicked.connect(self.ignore) ignoreAllButton = QPushButton(_('&Ignore All')) rightLayout.addWidget(ignoreAllButton) ignoreAllButton.clicked.connect(self.ignoreAll) rightLayout.addStretch() addButton = QPushButton(_('&Add')) rightLayout.addWidget(addButton) addButton.clicked.connect(self.add) addLowerButton = QPushButton(_('Add &Lowercase')) rightLayout.addWidget(addLowerButton) addLowerButton.clicked.connect(self.addLower) rightLayout.addStretch() replaceButton = QPushButton(_('&Replace')) rightLayout.addWidget(replaceButton) replaceButton.clicked.connect(self.replace) self.replaceAllButton = QPushButton(_('Re&place All')) rightLayout.addWidget(self.replaceAllButton) self.replaceAllButton.clicked.connect(self.replaceAll) rightLayout.addStretch() cancelButton = QPushButton(_('&Cancel')) rightLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.widgetDisableList = [ignoreButton, ignoreAllButton, addButton, addLowerButton, self.suggestList] self.fullDisableList = (self.widgetDisableList + [self.replaceAllButton, self.wordEdit]) def startSpellCheck(self, textLineIter): """Spell check text lines given in the iterator. Block execution except for the dialog if mispellings are found. Return True if spell check completes, False if cancelled. Arguments: textLineIter -- an iterator of text lines to check """ self.textLineIter = textLineIter try: self.textLine = next(self.textLineIter) except StopIteration: return True if self.spellCheck(): if self.exec_() == QDialog.Rejected: return False return True def continueSpellCheck(self): """Check lines, starting with current line. Exit the dialog if there are no more lines to check. """ if not self.spellCheck(): self.accept() def spellCheck(self): """Step through the iterator and spell check the lines. If results found, update the dialog with the results and return True. Return false if the end of the iterator is reached. """ while True: results = self.spellCheckInterface.checkLine(self.textLine, self.tmpIgnoreWords) if results: self.word, self.position, suggestions = results[0] newWord = self.replaceAllDict.get(self.word, '') if newWord: self.textLine = self.replaceWord(newWord) self.changeRequest.emit(self.textLine) else: self.misspellFound.emit() self.setWord(suggestions) return True try: self.textLine = next(self.textLineIter) self.tmpIgnoreWords.clear() except StopIteration: return False def setWord(self, suggestions): """Set dialog contents from the checked line and spell check results. Arguments: suggestions -- a list of suggested replacement words """ 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 with newWord. Arguments: newWord -- the replacement word """ return (self.textLine[:self.position] + newWord + self.textLine[self.position + len(self.word):]) def ignore(self): """Set word to ignored (this check only) and continue spell check. """ self.tmpIgnoreWords.add(self.word) self.continueSpellCheck() def ignoreAll(self): """Add to dictionary's ignore list and continue spell check. """ self.spellCheckInterface.acceptWord(self.word) self.continueSpellCheck() def add(self): """Add misspelling to dictionary and continue spell check""" self.spellCheckInterface.addToDict(self.word, False) self.continueSpellCheck() def addLower(self): """Add misspelling to dictionary as lowercase and continue spell check. """ self.spellCheckInterface.addToDict(self.word, True) self.continueSpellCheck() def replace(self): """Replace misspelled word with suggestion or context edit box Then continue spell check. """ if self.suggestList.isEnabled(): newWord = self.suggestList.currentItem().text() self.textLine = self.replaceWord(newWord) else: self.textLine = self.contextEdit.toPlainText() self.changeRequest.emit(self.textLine) self.continueSpellCheck() def replaceAll(self): """Replace misspelled word with suggestion or word edit (in future too). Stores changed word in replaceAllDict and continues spell check. """ if self.suggestList.isEnabled(): newWord = self.suggestList.currentItem().text() else: newWord = self.wordEdit.text() self.textLine = self.replaceWord(newWord) self.replaceAllDict[self.word] = newWord self.changeRequest.emit(self.textLine) self.continueSpellCheck() def updateFromWord(self): """Update dialog after word line editor change. Disables suggests and ignore/add controls. Updates the context editor. """ for widget in self.widgetDisableList: widget.setEnabled(False) newWord = 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. Disables controls except for replace. """ for widget in self.fullDisableList: widget.setEnabled(False) self.suggestList.clearSelection() class SpellContextEdit(QTextEdit): """Editor for spell check word context. Sets the size hint to 3 lines and simplifies selction. """ def __init__(self, parent=None): """Create the editor. Arguments: parent -- the parent widget """ super().__init__(parent) self.setTabChangesFocus(True) def sizeHint(self): """Set prefered size of 3 lines long. """ fontHeight = QFontMetrics(self.currentFont()).lineSpacing() return QSize(QTextEdit.sizeHint(self).width(), fontHeight * 3) def setSelection(self, fromPos, toPos): """Select the given range in first paragraph. Arguments: fromPos -- the starting position toPos -- the ending position """ cursor = self.textCursor() cursor.setPosition(fromPos) cursor.setPosition(toPos, QTextCursor.KeepAnchor) self.setTextCursor(cursor) self.ensureCursorVisible() TreeLine/source/dataeditors.py0000644000175000017500000032037013676201144015431 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # dataeditors.py, provides classes for data editors in the data edit view # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import xml.sax.saxutils import os.path import sys import re import math import enum import datetime import subprocess from PyQt5.QtCore import (QDate, QDateTime, QPoint, QPointF, QRect, QSize, QTime, Qt, pyqtSignal) from PyQt5.QtGui import (QBrush, QFont, QFontMetrics, QPainter, QPainterPath, QPixmap, QPen, QTextCursor, QTextDocument, QValidator) from PyQt5.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QAction, QApplication, QButtonGroup, QCalendarWidget, QCheckBox, QColorDialog, QComboBox, QDialog, QFileDialog, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMenu, QPushButton, QRadioButton, QScrollArea, QSizePolicy, QSpinBox, QTextEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget) import dataeditview import fieldformat import urltools import globalref import optiondefaults multipleSpaceRegEx = re.compile(r' {2,}') class PlainTextEditor(QTextEdit): """An editor widget for multi-line plain text fields. """ dragLinkEnabled = False contentsChanged = pyqtSignal(QWidget) editEnding = pyqtSignal(QWidget) keyPressed = pyqtSignal(QWidget) def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.setAcceptRichText(False) self.setPalette(QApplication.palette()) self.setStyleSheet('QTextEdit {border: 2px solid palette(highlight)}') self.setTabChangesFocus(True) self.cursorPositionChanged.connect(self.updateActions) self.selectionChanged.connect(self.updateActions) self.allActions = parent.parent().allActions self.modified = False self.textChanged.connect(self.signalUpdate) self.allActions['FormatInsertDate'].triggered.connect(self.insDate) def setContents(self, text): """Set the contents of the editor to text. Arguments: text - the new text contents for the editor """ self.blockSignals(True) self.setPlainText(text) self.blockSignals(False) def contents(self): """Return the editor text contents. """ return self.toPlainText() def hasSelectedText(self): """Return True if text is selected. """ return self.textCursor().hasSelection() def cursorPosTuple(self): """Return a tuple of the current cursor position and anchor (integers). """ cursor = self.textCursor() return (cursor.anchor(), cursor.position()) def setCursorPos(self, anchor, position): """Set the cursor to the given anchor and position. Arguments: anchor -- the cursor selection start integer position -- the cursor position or select end integer """ cursor = self.textCursor() cursor.setPosition(anchor) cursor.setPosition(position, QTextCursor.KeepAnchor) self.setTextCursor(cursor) # self.ensureCursorVisible() def setCursorPoint(self, point): """Set the cursor to the given point. Arguments: point -- the QPoint for the new cursor position """ self.setTextCursor(self.cursorForPosition(self.mapFromGlobal(point))) def resetCursor(self): """Set the cursor to end for tab-focus use. """ self.moveCursor(QTextCursor.End) def scrollPosition(self): """Return the current scrollbar position. """ return self.verticalScrollBar().value() def setScrollPosition(self, value): """Set the scrollbar position to value. Arguments: value -- the new scrollbar position """ self.verticalScrollBar().setValue(value) def signalUpdate(self): """Signal the delegate to update the model based on an editor change. """ self.modified = True self.contentsChanged.emit(self) def disableActions(self): """Reset action availability after focus is lost. """ self.allActions['EditCut'].setEnabled(True) self.allActions['EditCopy'].setEnabled(True) mime = QApplication.clipboard().mimeData() self.allActions['EditPaste'].setEnabled(len(mime.data('text/xml') or mime.data('text/plain')) > 0) self.allActions['FormatInsertDate'].setEnabled(False) def updateActions(self): """Set availability of context menu actions. """ hasSelection = self.textCursor().hasSelection() self.allActions['EditCut'].setEnabled(hasSelection) self.allActions['EditCopy'].setEnabled(hasSelection) mime = QApplication.clipboard().mimeData() self.allActions['EditPaste'].setEnabled(len(mime.data('text/plain')) > 0) self.allActions['FormatInsertDate'].setEnabled(True) def insDate(self): """Insert the current date using the editor format. """ date = datetime.date.today() editorFormat = fieldformat.adjOutDateFormat(globalref. genOptions['EditDateFormat']) dateText = date.strftime(editorFormat) self.insertPlainText(dateText) def contextMenuEvent(self, event): """Override popup menu to add global actions. Arguments: event -- the menu event """ menu = QMenu(self) menu.addAction(self.allActions['FormatSelectAll']) menu.addSeparator() menu.addAction(self.allActions['EditCut']) menu.addAction(self.allActions['EditCopy']) menu.addAction(self.allActions['EditPaste']) menu.addSeparator() menu.addAction(self.allActions['FormatInsertDate']) menu.exec_(event.globalPos()) def focusInEvent(self, event): """Set availability and update format actions. Arguments: event -- the focus event """ super().focusInEvent(event) self.updateActions() def focusOutEvent(self, event): """Reset format actions on focus loss if not focusing a menu. Arguments: event -- the focus event """ super().focusOutEvent(event) if event.reason() != Qt.PopupFocusReason: self.disableActions() self.editEnding.emit(self) def hideEvent(self, event): """Reset format actions when the editor is hidden. Arguments: event -- the hide event """ self.disableActions() self.editEnding.emit(self) super().hideEvent(event) def keyPressEvent(self, event): """Emit a signal after every key press and handle page up/down. Needed to adjust scroll position in unlimited height editors. Arguments: event -- the key press event """ if (event.key() in (Qt.Key_PageUp, Qt.Key_PageDown) and not globalref.genOptions['EditorLimitHeight']): pos = self.cursorRect().center() if event.key() == Qt.Key_PageUp: pos.setY(pos.y() - self.parent().height()) if pos.y() < 0: pos.setY(0) else: pos.setY(pos.y() + self.parent().height()) if pos.y() > self.height(): pos.setY(self.height()) newCursor = self.cursorForPosition(pos) if event.modifiers() == Qt.ShiftModifier: cursor = self.textCursor() cursor.setPosition(newCursor.position(), QTextCursor.KeepAnchor) self.setTextCursor(cursor) else: self.setTextCursor(newCursor) event.accept() self.keyPressed.emit(self) return super().keyPressEvent(event) self.keyPressed.emit(self) class HtmlTextEditor(PlainTextEditor): """An editor for HTML fields, plain text with HTML insert commands. """ htmlFontSizes = ('small', '', 'large', 'x-large', 'xx-large') dragLinkEnabled = True inLinkSelectMode = pyqtSignal(bool) def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.intLinkDialog = None self.nodeRef = None self.allActions['FormatBoldFont'].triggered.connect(self.setBoldFont) self.allActions['FormatItalicFont'].triggered.connect(self. setItalicFont) self.allActions['FormatUnderlineFont'].triggered.connect(self. setUnderlineFont) self.allActions['FormatFontSize'].parent().triggered.connect(self. setFontSize) self.allActions['FormatFontSize'].triggered.connect(self. showFontSizeMenu) self.allActions['FormatFontColor'].triggered.connect(self.setFontColor) self.allActions['FormatExtLink'].triggered.connect(self.setExtLink) self.allActions['FormatIntLink'].triggered.connect(self.setIntLink) def insertTagText(self, prefix, suffix): """Insert given tag text and maintain the original selection. Arguments: prefix -- the opening tag suffix -- the closing tag """ cursor = self.textCursor() start = cursor.selectionStart() end = cursor.selectionEnd() text = '{0}{1}{2}'.format(prefix, cursor.selectedText(), suffix) self.insertPlainText(text) cursor.setPosition(start + len(prefix)) cursor.setPosition(end + len(prefix), QTextCursor.KeepAnchor) self.setTextCursor(cursor) def setBoldFont(self, checked): """Insert tags for a bold font. Arguments: checked -- current toggle state of the control """ try: if self.hasFocus() and checked: self.insertTagText('', '') except RuntimeError: pass # avoid calling a deleted C++ editor object def setItalicFont(self, checked): """Insert tags for an italic font. Arguments: checked -- current toggle state of the control """ try: if self.hasFocus() and checked: self.insertTagText('', '') except RuntimeError: pass # avoid calling a deleted C++ editor object def setUnderlineFont(self, checked): """Insert tags for an underline font. Arguments: checked -- current toggle state of the control """ try: if self.hasFocus() and checked: self.insertTagText('', '') except RuntimeError: pass # avoid calling a deleted C++ editor object def setFontSize(self, action): """Set the font size of the selection or the current setting. Arguments: action -- the sub-menu action that was picked """ try: if self.hasFocus(): actions = self.allActions['FormatFontSize'].parent().actions() sizeNum = actions.index(action) size = HtmlTextEditor.htmlFontSizes[sizeNum] self.insertTagText(''.format(size), '') except RuntimeError: pass # avoid calling a deleted C++ editor object def setFontColor(self): """Set the font color of the selection or the current setting. Prompt the user for a color using a dialog. """ try: if self.hasFocus(): charFormat = self.currentCharFormat() oldColor = charFormat.foreground().color() newColor = QColorDialog.getColor(oldColor, self) if newColor.isValid(): self.insertTagText(''. format(newColor.name()), '') except RuntimeError: pass # avoid calling a deleted C++ editor object def setExtLink(self): """Add or modify an extrnal web link at the cursor. """ try: if self.hasFocus(): dialog = ExtLinkDialog(False, self) address, name = self.selectLink() if address.startswith('#'): address = name = '' dialog.setFromComponents(address, name) if dialog.exec_() == QDialog.Accepted: self.insertPlainText(dialog.htmlText()) except RuntimeError: pass # avoid calling a deleted C++ editor object def setIntLink(self): """Show dialog to add or modify an internal node link at the cursor. """ try: if self.hasFocus(): self.intLinkDialog = EmbedIntLinkDialog(self.nodeRef. treeStructureRef(), self) address, name = self.selectLink() if address.startswith('#'): address = address.lstrip('#') else: address = '' self.intLinkDialog.setFromComponents(address, name) self.intLinkDialog.finished.connect(self.insertInternalLink) self.intLinkDialog.show() self.inLinkSelectMode.emit(True) except RuntimeError: pass # avoid calling a deleted C++ editor object def insertInternalLink(self, resultCode): """Add or modify an internal node link based on dialog approval. Arguments: resultCode -- the result from the dialog (OK or cancel) """ if resultCode == QDialog.Accepted: self.insertPlainText(self.intLinkDialog.htmlText()) self.intLinkDialog = None self.inLinkSelectMode.emit(False) def setLinkFromNode(self, node): """Set the current internal link from a clicked node. Arguments: node -- the node to set the unique ID from """ if self.intLinkDialog: self.intLinkDialog.setFromNode(node) def selectLink(self): """Select the full link at the cursor, return link data. Any links at the cursor or partially selected are fully selected. Returns a tuple of the link address and name, or a tuple with empty strings if none are found. """ cursor = self.textCursor() anchor = cursor.anchor() position = cursor.position() for match in fieldformat.linkRegExp.finditer(self.toPlainText()): start = match.start() end = match.end() if start < anchor < end or start < position < end: address, name = match.groups() cursor.setPosition(start) cursor.setPosition(end, QTextCursor.KeepAnchor) self.setTextCursor(cursor) return (address, name) return ('', cursor.selectedText()) def addDroppedUrl(self, urlText): """Add the URL link that was dropped on this editor from the view. Arguments: urlText -- the text of the link """ name = urltools.shortName(urlText) text = '{1}'.format(urlText, name) self.insertPlainText(text) def disableActions(self): """Set format actions to unavailable. """ super().disableActions() self.allActions['FormatBoldFont'].setEnabled(False) self.allActions['FormatItalicFont'].setEnabled(False) self.allActions['FormatUnderlineFont'].setEnabled(False) self.allActions['FormatFontSize'].parent().setEnabled(False) self.allActions['FormatFontColor'].setEnabled(False) self.allActions['FormatExtLink'].setEnabled(False) self.allActions['FormatIntLink'].setEnabled(False) def updateActions(self): """Set editor format actions to available and update toggle states. """ super().updateActions() boldFontAct = self.allActions['FormatBoldFont'] boldFontAct.setEnabled(True) boldFontAct.setChecked(False) italicAct = self.allActions['FormatItalicFont'] italicAct.setEnabled(True) italicAct.setChecked(False) underlineAct = self.allActions['FormatUnderlineFont'] underlineAct.setEnabled(True) underlineAct.setChecked(False) fontSizeSubMenu = self.allActions['FormatFontSize'].parent() fontSizeSubMenu.setEnabled(True) for action in fontSizeSubMenu.actions(): action.setChecked(False) self.allActions['FormatFontColor'].setEnabled(True) self.allActions['FormatExtLink'].setEnabled(True) self.allActions['FormatIntLink'].setEnabled(True) def showFontSizeMenu(self): """Show a context menu for font size at this edit box. """ if self.hasFocus(): rect = self.rect() pt = self.mapToGlobal(QPoint(rect.center().x(), rect.bottom())) self.allActions['FormatFontSize'].parent().popup(pt) def contextMenuEvent(self, event): """Override popup menu to add formatting and global actions. Arguments: event -- the menu event """ menu = QMenu(self) menu.addAction(self.allActions['FormatBoldFont']) menu.addAction(self.allActions['FormatItalicFont']) menu.addAction(self.allActions['FormatUnderlineFont']) menu.addSeparator() menu.addMenu(self.allActions['FormatFontSize'].parent()) menu.addAction(self.allActions['FormatFontColor']) menu.addSeparator() menu.addAction(self.allActions['FormatExtLink']) menu.addAction(self.allActions['FormatIntLink']) menu.addAction(self.allActions['FormatInsertDate']) menu.addSeparator() menu.addAction(self.allActions['FormatSelectAll']) menu.addSeparator() menu.addAction(self.allActions['EditCut']) menu.addAction(self.allActions['EditCopy']) menu.addAction(self.allActions['EditPaste']) menu.exec_(event.globalPos()) def hideEvent(self, event): """Close the internal link dialog when the editor is hidden. Arguments: event -- the hide event """ if self.intLinkDialog: self.intLinkDialog.close() self.intLinkDialog = None super().hideEvent(event) class RichTextEditor(HtmlTextEditor): """An editor widget for multi-line wysiwyg rich text fields. """ fontPointSizes = [] def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.setAcceptRichText(True) if not RichTextEditor.fontPointSizes: doc = QTextDocument() doc.setDefaultFont(self.font()) for sizeName in HtmlTextEditor.htmlFontSizes: if sizeName: doc.setHtml('text'. format(sizeName)) pointSize = (QTextCursor(doc).charFormat().font(). pointSize()) else: pointSize = self.font().pointSize() RichTextEditor.fontPointSizes.append(pointSize) self.allActions['FormatClearFormat'].triggered.connect(self. setClearFormat) self.allActions['EditPastePlain'].triggered.connect(self.pastePlain) def setContents(self, text): """Set the contents of the editor to text. Arguments: text - the new text contents for the editor """ self.blockSignals(True) self.setHtml(text) self.blockSignals(False) def contents(self): """Return simplified HTML code for the editor contents. Replace Unicode line feeds with HTML breaks, escape <, >, &, and replace some rich formatting with HTML tags. """ doc = self.document() block = doc.begin() result = '' while block.isValid(): if result: result += '
    ' fragIter = block.begin() while not fragIter.atEnd(): text = xml.sax.saxutils.escape(fragIter.fragment().text()) text = text.replace('\u2028', '
    ') charFormat = fragIter.fragment().charFormat() if charFormat.fontWeight() >= QFont.Bold: text = '{0}'.format(text) if charFormat.fontItalic(): text = '{0}'.format(text) size = charFormat.font().pointSize() if size != self.font().pointSize(): closeSize = min((abs(size - i), i) for i in RichTextEditor.fontPointSizes)[1] sizeNum = RichTextEditor.fontPointSizes.index(closeSize) htmlSize = HtmlTextEditor.htmlFontSizes[sizeNum] if htmlSize: text = ('{1}'. format(htmlSize, text)) if charFormat.anchorHref(): text = '{1}'.format(charFormat. anchorHref(), text) else: # ignore underline and font color for links if charFormat.fontUnderline(): text = '{0}'.format(text) if (charFormat.foreground().color().name() != block.charFormat().foreground().color().name()): text = ('{1}'. format(charFormat.foreground().color().name(), text)) result += text fragIter += 1 block = block.next() return result def setBoldFont(self, checked): """Set the selection or the current setting to a bold font. Arguments: checked -- current toggle state of the control """ try: if self.hasFocus(): if checked: self.setFontWeight(QFont.Bold) else: self.setFontWeight(QFont.Normal) except RuntimeError: pass # avoid calling a deleted C++ editor object def setItalicFont(self, checked): """Set the selection or the current setting to an italic font. Arguments: checked -- current toggle state of the control """ try: if self.hasFocus(): self.setFontItalic(checked) except RuntimeError: pass # avoid calling a deleted C++ editor object def setUnderlineFont(self, checked): """Set the selection or the current setting to an underlined font. Arguments: checked -- current toggle state of the control """ try: if self.hasFocus(): self.setFontUnderline(checked) except RuntimeError: pass # avoid calling a deleted C++ editor object def setFontSize(self, action): """Set the font size of the selection or the current setting. Arguments: action -- the sub-menu action that was picked """ try: if self.hasFocus(): actions = self.allActions['FormatFontSize'].parent().actions() sizeNum = actions.index(action) pointSize = RichTextEditor.fontPointSizes[sizeNum] charFormat = self.currentCharFormat() charFormat.setFontPointSize(pointSize) self.setCurrentCharFormat(charFormat) except RuntimeError: pass # avoid calling a deleted C++ editor object def setFontColor(self): """Set the font color of the selection or the current setting. Prompt the user for a color using a dialog. """ try: if self.hasFocus(): charFormat = self.currentCharFormat() oldColor = charFormat.foreground().color() newColor = QColorDialog.getColor(oldColor, self) if newColor.isValid(): charFormat.setForeground(QBrush(newColor)) self.setCurrentCharFormat(charFormat) except RuntimeError: pass # avoid calling a deleted C++ editor object def setClearFormat(self): """Clear the current or selected text formatting. """ try: if self.hasFocus(): self.setCurrentFont(self.font()) charFormat = self.currentCharFormat() charFormat.clearForeground() charFormat.setAnchor(False) charFormat.setAnchorHref('') self.setCurrentCharFormat(charFormat) except RuntimeError: pass # avoid calling a deleted C++ editor object def setExtLink(self): """Add or modify an extrnal web link at the cursor. """ try: if self.hasFocus(): dialog = ExtLinkDialog(False, self) address, name = self.selectLink() if address.startswith('#'): address = name = '' dialog.setFromComponents(address, name) if dialog.exec_() == QDialog.Accepted: if self.textCursor().hasSelection(): self.insertHtml(dialog.htmlText()) else: self.insertHtml(dialog.htmlText() + ' ') except RuntimeError: pass # avoid calling a deleted C++ editor object def insertInternalLink(self, resultCode): """Add or modify an internal node link based on dialog approval. Arguments: resultCode -- the result from the dialog (OK or cancel) """ if resultCode == QDialog.Accepted: if self.textCursor().hasSelection(): self.insertHtml(self.intLinkDialog.htmlText()) else: self.insertHtml(self.intLinkDialog.htmlText() + ' ') self.intLinkDialog = None self.inLinkSelectMode.emit(False) def selectLink(self): """Select the full link at the cursor, return link data. Any links at the cursor or partially selected are fully selected. Returns a tuple of the link address and name, or a tuple with empty strings if none are found. """ cursor = self.textCursor() if not cursor.hasSelection() and not cursor.charFormat().anchorHref(): return ('', '') selectText = cursor.selection().toPlainText() anchorCursor = QTextCursor(self.document()) anchorCursor.setPosition(cursor.anchor()) cursor.clearSelection() if cursor < anchorCursor: anchorCursor, cursor = cursor, anchorCursor position = cursor.position() address = name = '' if anchorCursor.charFormat().anchorHref(): fragIter = anchorCursor.block().begin() while not (fragIter.fragment().contains(anchorCursor.position()) or fragIter.fragment().contains(anchorCursor.position() - 1)): fragIter += 1 fragment = fragIter.fragment() anchorCursor.setPosition(fragment.position()) address = fragment.charFormat().anchorHref() name = fragment.text() if cursor.charFormat().anchorHref(): fragIter = cursor.block().begin() while not (fragIter.fragment().contains(cursor.position()) or fragIter.fragment().contains(cursor.position() - 1)): fragIter += 1 fragment = fragIter.fragment() position = fragment.position() + fragment.length() address = fragment.charFormat().anchorHref() name = fragment.text() if not name: name = selectText.split('\n')[0] cursor.setPosition(anchorCursor.position()) cursor.setPosition(position, QTextCursor.KeepAnchor) self.setTextCursor(cursor) return (address, name) def addDroppedUrl(self, urlText): """Add the URL link that was dropped on this editor from the view. Arguments: urlText -- the text of the link """ name = urltools.shortName(urlText) text = '{1}'.format(urlText, name) if not self.textCursor().hasSelection(): text += ' ' self.insertHtml(text) def pastePlain(self): """Paste non-formatted text from the clipboard. """ text = QApplication.clipboard().mimeData().text() if text and self.hasFocus(): self.insertPlainText(text) def disableActions(self): """Set format actions to unavailable. """ super().disableActions() self.allActions['FormatClearFormat'].setEnabled(False) self.allActions['EditPastePlain'].setEnabled(False) def updateActions(self): """Set editor format actions to available and update toggle states. """ super().updateActions() self.allActions['FormatBoldFont'].setChecked(self.fontWeight() == QFont.Bold) self.allActions['FormatItalicFont'].setChecked(self.fontItalic()) self.allActions['FormatUnderlineFont'].setChecked(self.fontUnderline()) fontSizeSubMenu = self.allActions['FormatFontSize'].parent() pointSize = int(self.fontPointSize()) try: sizeNum = RichTextEditor.fontPointSizes.index(pointSize) except ValueError: sizeNum = 1 # default size fontSizeSubMenu.actions()[sizeNum].setChecked(True) self.allActions['FormatClearFormat'].setEnabled(True) mime = QApplication.clipboard().mimeData() self.allActions['EditPastePlain'].setEnabled(len(mime. data('text/plain')) > 0) def contextMenuEvent(self, event): """Override popup menu to add formatting and global actions. Arguments: event -- the menu event """ menu = QMenu(self) menu.addAction(self.allActions['FormatBoldFont']) menu.addAction(self.allActions['FormatItalicFont']) menu.addAction(self.allActions['FormatUnderlineFont']) menu.addSeparator() menu.addMenu(self.allActions['FormatFontSize'].parent()) menu.addAction(self.allActions['FormatFontColor']) menu.addSeparator() menu.addAction(self.allActions['FormatExtLink']) menu.addAction(self.allActions['FormatIntLink']) menu.addAction(self.allActions['FormatInsertDate']) menu.addSeparator() menu.addAction(self.allActions['FormatSelectAll']) menu.addAction(self.allActions['FormatClearFormat']) menu.addSeparator() menu.addAction(self.allActions['EditCut']) menu.addAction(self.allActions['EditCopy']) menu.addAction(self.allActions['EditPaste']) menu.addAction(self.allActions['EditPastePlain']) menu.exec_(event.globalPos()) def mousePressEvent(self, event): """Handle ctrl + click to follow links. Arguments: event -- the mouse event """ if (event.button() == Qt.LeftButton and event.modifiers() == Qt.ControlModifier): cursor = self.cursorForPosition(event.pos()) address = cursor.charFormat().anchorHref() if address: if address.startswith('#'): editView = self.parent().parent() selectModel = editView.treeView.selectionModel() selectModel.selectNodeById(address[1:]) else: # check for relative path if urltools.isRelative(address): defaultPath = str(globalref.mainControl. defaultPathObj(True)) address = urltools.toAbsolute(address, defaultPath) openExtUrl(address) event.accept() else: super().mousePressEvent(event) class OneLineTextEditor(RichTextEditor): """An editor widget for single-line wysiwyg rich text fields. """ def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) def insertFromMimeData(self, mimeSource): """Override to verify that only a single line is pasted or dropped. Arguments: mimeSource -- the mime source to be inserted """ super().insertFromMimeData(mimeSource) text = self.contents() if '
    ' in text: text = text.split('
    ', 1)[0] self.blockSignals(True) self.setHtml(text) self.blockSignals(False) self.moveCursor(QTextCursor.End) def keyPressEvent(self, event): """Customize handling of return and control keys. Arguments: event -- the key press event """ if event.key() not in (Qt.Key_Enter, Qt.Key_Return): super().keyPressEvent(event) class LineEditor(QLineEdit): """An editor widget for unformatted single-line fields. Used both stand-alone and as part of the combo box editor. """ dragLinkEnabled = False contentsChanged = pyqtSignal(QWidget) editEnding = pyqtSignal(QWidget) contextMenuPrep = pyqtSignal() def __init__(self, parent=None, subControl=False): """Initialize the editor class. Includes a colored triangle error flag for non-matching formats. Arguments: parent -- the parent, if given subcontrol -- true if used inside a combo box (no border or signal) """ super().__init__(parent) self.setPalette(QApplication.palette()) self.cursorPositionChanged.connect(self.updateActions) self.selectionChanged.connect(self.updateActions) try: self.allActions = parent.parent().allActions except AttributeError: # view is a level up if embedded in a combo self.allActions = parent.parent().parent().allActions self.modified = False self.errorFlag = False self.savedCursorPos = None self.extraMenuActions = [] if not subControl: self.setStyleSheet('QLineEdit {border: 2px solid ' 'palette(highlight)}') self.textEdited.connect(self.signalUpdate) def setContents(self, text): """Set the contents of the editor to text. Arguments: text - the new text contents for the editor """ self.setText(text) def contents(self): """Return the editor text contents. """ return self.text() def signalUpdate(self): """Signal the delegate to update the model based on an editor change. """ self.modified = True self.errorFlag = False self.contentsChanged.emit(self) def setErrorFlag(self): """Set the error flag to True and repaint the widget. """ self.errorFlag = True self.update() def cursorPosTuple(self): """Return a tuple of the current cursor position and anchor (integers). """ pos = start = self.cursorPosition() if self.hasSelectedText(): start = self.selectionStart() return (start, pos) def setCursorPos(self, anchor, position): """Set the cursor to the given anchor and position. Arguments: anchor -- the cursor selection start integer position -- the cursor position or select end integer """ if anchor == position: self.deselect() self.setCursorPosition(position) else: self.setSelection(anchor, position - anchor) def setCursorPoint(self, point): """Set the cursor to the given point. Arguments: point -- the QPoint for the new cursor position """ self.savedCursorPos = self.cursorPositionAt(self.mapFromGlobal(point)) self.setCursorPosition(self.savedCursorPos) def resetCursor(self): """Set the cursor to select all for tab-focus use. """ self.selectAll() def scrollPosition(self): """Return the current scrollbar position. """ return 0 def setScrollPosition(self, value): """Set the scrollbar position to value. No operation with single line editor. Arguments: value -- the new scrollbar position """ pass def paintEvent(self, event): """Add painting of the error flag to the paint event. Arguments: event -- the paint event """ super().paintEvent(event) if self.errorFlag: painter = QPainter(self) path = QPainterPath(QPointF(0, 0)) path.lineTo(0, 10) path.lineTo(10, 0) path.closeSubpath() painter.fillPath(path, QApplication.palette().highlight()) def disableActions(self): """Reset action availability after focus is lost. """ self.allActions['EditCut'].setEnabled(True) self.allActions['EditCopy'].setEnabled(True) mime = QApplication.clipboard().mimeData() self.allActions['EditPaste'].setEnabled(len(mime.data('text/xml') or mime.data('text/plain')) > 0) def updateActions(self): """Set availability of context menu actions. """ hasSelection = self.hasSelectedText() self.allActions['EditCut'].setEnabled(hasSelection) self.allActions['EditCopy'].setEnabled(hasSelection) mime = QApplication.clipboard().mimeData() self.allActions['EditPaste'].setEnabled(len(mime.data('text/plain')) > 0) def contextMenuEvent(self, event): """Override popup menu to add formatting actions. Arguments: event -- the menu event """ self.contextMenuPrep.emit() menu = QMenu(self) if self.extraMenuActions: for action in self.extraMenuActions: menu.addAction(action) menu.addSeparator() menu.addAction(self.allActions['FormatSelectAll']) menu.addSeparator() menu.addAction(self.allActions['EditCut']) menu.addAction(self.allActions['EditCopy']) menu.addAction(self.allActions['EditPaste']) menu.exec_(event.globalPos()) def focusInEvent(self, event): """Restore a saved cursor position for new editors. Arguments: event -- the focus event """ super().focusInEvent(event) if (event.reason() == Qt.OtherFocusReason and self.savedCursorPos != None): self.setCursorPosition(self.savedCursorPos) self.savedCursorPos = None self.updateActions() def focusOutEvent(self, event): """Reset format actions on focus loss if not focusing a menu. Arguments: event -- the focus event """ super().focusOutEvent(event) if event.reason() != Qt.PopupFocusReason: self.disableActions() self.editEnding.emit(self) def hideEvent(self, event): """Reset format actions when the editor is hidden. Arguments: event -- the hide event """ self.disableActions() self.editEnding.emit(self) super().hideEvent(event) class ReadOnlyEditor(LineEditor): """An editor widget that doesn't allow any edits. """ def __init__(self, parent=None): """Initialize the editor class. Includes a colored triangle error flag for non-matching formats. Arguments: parent -- the parent, if given """ super().__init__(parent, True) self.setReadOnly(True) self.setStyleSheet('QLineEdit {border: 2px solid palette(highlight); ' 'background-color: palette(button)}') class ComboEditor(QComboBox): """A general combo box editor widget. Uses the LineEditor class to paint the error flag. """ dragLinkEnabled = False contentsChanged = pyqtSignal(QWidget) editEnding = pyqtSignal(QWidget) def __init__(self, parent=None): """Initialize the editor class. The self.fieldRef and self.nodeRef must be set after creation. Arguments: parent -- the parent, if given """ super().__init__(parent) self.setPalette(QApplication.palette()) self.setStyleSheet('QComboBox {border: 2px solid palette(highlight)}') self.setEditable(True) self.setLineEdit(LineEditor(self, True)) self.listView = QTreeWidget() self.listView.setColumnCount(2) self.listView.header().hide() self.listView.setRootIsDecorated(False) self.listView.setSelectionBehavior(QAbstractItemView.SelectRows) self.listView.header().setSectionResizeMode(QHeaderView. ResizeToContents) self.setModel(self.listView.model()) self.setView(self.listView) self.setModelColumn(0) self.modified = False self.fieldRef = None self.nodeRef = None self.editTextChanged.connect(self.signalUpdate) self.lineEdit().editEnding.connect(self.signalEditEnd) def setContents(self, text): """Set the contents of the editor to text. Arguments: text - the new text contents for the editor """ self.blockSignals(True) self.setEditText(text) self.blockSignals(False) def contents(self): """Return the editor text contents. """ return self.currentText() def showPopup(self): """Load combo box with choices before showing it. """ self.listView.setColumnCount(self.fieldRef.numChoiceColumns) text = self.currentText() if self.fieldRef.autoAddChoices: self.fieldRef.clearChoices() for node in self.nodeRef.treeStructureRef().nodeDict.values(): if node.formatRef == self.nodeRef.formatRef: self.fieldRef.addChoice(node.data.get(self.fieldRef.name, '')) self.blockSignals(True) self.clear() if self.fieldRef.numChoiceColumns == 1: choices = self.fieldRef.comboChoices() self.addItems(choices) else: annotatedChoices = self.fieldRef.annotatedComboChoices(text) for choice, annot in annotatedChoices: QTreeWidgetItem(self.listView, [choice, annot]) choices = [choice for (choice, annot) in annotatedChoices] try: self.setCurrentIndex(choices.index(text)) except ValueError: self.setEditText(text) self.blockSignals(False) super().showPopup() def signalUpdate(self): """Signal the delegate to update the model based on an editor change. """ self.modified = True self.lineEdit().errorFlag = False self.contentsChanged.emit(self) def setErrorFlag(self): """Set the error flag to True and repaint the widget. """ self.lineEdit().errorFlag = True self.update() def hasSelectedText(self): """Return True if text is selected. """ return self.lineEdit().hasSelectedText() def selectAll(self): """Select all text in the line editor. """ self.lineEdit().selectAll() def cursorPosTuple(self): """Return a tuple of the current cursor position and anchor (integers). """ return self.lineEdit().cursorPosTuple() def setCursorPos(self, anchor, position): """Set the cursor to the given anchor and position. Arguments: anchor -- the cursor selection start integer position -- the cursor position or select end integer """ self.lineEdit().setCursorPos(anchor, position) def setCursorPoint(self, point): """Set the cursor to the given point. Arguments: point -- the QPoint for the new cursor position """ self.lineEdit().setCursorPoint(point) def resetCursor(self): """Set the cursor to select all for tab-focus use. """ self.lineEdit().selectAll() def scrollPosition(self): """Return the current scrollbar position. """ return 0 def setScrollPosition(self, value): """Set the scrollbar position to value. No operation with single line editor. Arguments: value -- the new scrollbar position """ pass def copy(self): """Copy text selected in the line editor. """ self.lineEdit().copy() def cut(self): """Cut text selected in the line editor. """ self.lineEdit().cut() def paste(self): """Paste from the clipboard into the line editor. """ self.lineEdit().paste() def signalEditEnd(self): """Emit editEnding signal based on line edit signal. """ self.editEnding.emit(self) class CombinationEditor(ComboEditor): """An editor widget for combination and auto-combination fields. Uses a combo box with a list of checkboxes in place of the list popup. """ def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.checkBoxDialog = None def showPopup(self): """Override to show a popup entry widget in place of a list view. """ if self.fieldRef.autoAddChoices: self.fieldRef.clearChoices() for node in self.nodeRef.treeStructureRef().nodeDict.values(): if node.formatRef == self.nodeRef.formatRef: self.fieldRef.addChoice(node.data.get(self.fieldRef.name, '')) selectList = self.fieldRef.comboActiveChoices(self.currentText()) self.checkBoxDialog = CombinationDialog(self.fieldRef.comboChoices(), selectList, self) self.checkBoxDialog.setMinimumWidth(self.width()) self.checkBoxDialog.buttonChanged.connect(self.updateText) self.checkBoxDialog.show() pos = self.mapToGlobal(self.rect().bottomRight()) pos.setX(pos.x() - self.checkBoxDialog.width() + 1) screenBottom = (QApplication.desktop().screenGeometry(self). bottom()) if pos.y() + self.checkBoxDialog.height() > screenBottom: pos.setY(pos.y() - self.rect().height() - self.checkBoxDialog.height()) self.checkBoxDialog.move(pos) def hidePopup(self): """Override to hide the popup entry widget. """ if self.checkBoxDialog: self.checkBoxDialog.hide() super().hidePopup() def updateText(self): """Update the text based on a changed signal. """ if self.checkBoxDialog: self.setEditText(self.fieldRef.joinText(self.checkBoxDialog. selectList())) class CombinationDialog(QDialog): """A popup dialog box for combination and auto-combination fields. """ buttonChanged = pyqtSignal() def __init__(self, choiceList, selectList, parent=None): """Initialize the combination dialog. Arguments: choiceList -- a list of text choices selectList -- a lit of choices to preselect parent -- the parent, if given """ super().__init__(parent) self.setWindowFlags(Qt.Popup) topLayout = QVBoxLayout(self) topLayout.setContentsMargins(0, 0, 0, 0) scrollArea = QScrollArea() scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) topLayout.addWidget(scrollArea) innerWidget = QWidget() innerLayout = QVBoxLayout(innerWidget) selected = set(selectList) self.buttonGroup = QButtonGroup(self) self.buttonGroup.setExclusive(False) self.buttonGroup.buttonClicked.connect(self.buttonChanged) for text in choiceList: button = QCheckBox(text, innerWidget) if text in selected: button.setChecked(True) self.buttonGroup.addButton(button) innerLayout.addWidget(button) scrollArea.setWidget(innerWidget) buttons = self.buttonGroup.buttons() if buttons: buttons[0].setFocus() def selectList(self): """Return a list of currently checked text. """ result = [] for button in self.buttonGroup.buttons(): if button.isChecked(): result.append(button.text()) return result class DateEditor(ComboEditor): """An editor widget for date fields. Uses a combo box with a calendar widget in place of the list popup. """ def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.calendar = None nowAction = QAction(_('Today\'s &Date'), self) nowAction.triggered.connect(self.setNow) self.lineEdit().extraMenuActions = [nowAction] def editorDate(self): """Return the date (as a QDate) set in the line editor. If none or invalid, return an invalid date. """ try: dateStr = self.fieldRef.storedText(self.currentText()) except ValueError: return QDate() return QDate.fromString(dateStr, Qt.ISODate) def showPopup(self): """Override to show a calendar widget in place of a list view. """ if not self.calendar: self.calendar = QCalendarWidget(self) self.calendar.setWindowFlags(Qt.Popup) weekStart = optiondefaults.daysOfWeek.index(globalref. genOptions['WeekStart']) self.calendar.setFirstDayOfWeek(weekStart + 1) self.calendar.setVerticalHeaderFormat(QCalendarWidget. NoVerticalHeader) self.calendar.clicked.connect(self.setDate) date = self.editorDate() if date.isValid(): self.calendar.setSelectedDate(date) self.calendar.show() pos = self.mapToGlobal(self.rect().bottomRight()) pos.setX(pos.x() - self.calendar.width()) screenBottom = (QApplication.desktop().screenGeometry(self). bottom()) if pos.y() + self.calendar.height() > screenBottom: pos.setY(pos.y() - self.rect().height() - self.calendar.height()) self.calendar.move(pos) def hidePopup(self): """Override to hide the calendar widget. """ if self.calendar: self.calendar.hide() super().hidePopup() def setDate(self, date): """Set the date based on a signal from the calendar popup. Arguments: date -- the QDate to be set """ dateStr = date.toString(Qt.ISODate) self.setEditText(self.fieldRef.formatEditorText(dateStr)) self.calendar.hide() def setNow(self): """Set to today's date. """ dateStr = QDate.currentDate().toString(Qt.ISODate) self.setEditText(self.fieldRef.formatEditorText(dateStr)) class TimeEditor(ComboEditor): """An editor widget for time fields. Adds a clock popup dialog and a "now" right-click menu action. """ def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.dialog = None nowAction = QAction(_('Set to &Now'), self) nowAction.triggered.connect(self.setNow) self.lineEdit().extraMenuActions = [nowAction] def showPopup(self): """Override to show a popup entry widget in place of a list view. """ if not self.dialog: self.dialog = TimeDialog(self) self.dialog.contentsChanged.connect(self.setTime) self.dialog.show() pos = self.mapToGlobal(self.rect().bottomRight()) pos.setX(pos.x() - self.dialog.width() + 1) screenBottom = QApplication.desktop().screenGeometry(self).bottom() if pos.y() + self.dialog.height() > screenBottom: pos.setY(pos.y() - self.rect().height() - self.dialog.height()) self.dialog.move(pos) try: storedText = self.fieldRef.storedText(self.currentText()) except ValueError: storedText = '' if storedText: self.dialog.setTimeFromText(storedText) def hidePopup(self): """Override to hide the popup entry widget. """ if self.dialog: self.dialog.hide() super().hidePopup() def setTime(self): """Set the time fom the dialog. """ if self.dialog: timeStr = self.dialog.timeObject().isoformat() + '.000' self.setEditText(self.fieldRef.formatEditorText(timeStr)) def setNow(self): """Set to the current time. """ timeStr = QTime.currentTime().toString('hh:mm:ss.zzz') self.setEditText(self.fieldRef.formatEditorText(timeStr)) TimeElem = enum.Enum('TimeElem', 'hour minute second') class TimeDialog(QDialog): """A popup clock dialog for time editing. """ contentsChanged = pyqtSignal() def __init__(self, addCalendar=False, parent=None): """Initialize the dialog widgets. Arguments: parent -- the dialog's parent widget """ super().__init__(parent) self.focusElem = None self.setWindowFlags(Qt.Popup) horizLayout = QHBoxLayout(self) if addCalendar: self.calendar = QCalendarWidget() horizLayout.addWidget(self.calendar) weekStart = optiondefaults.daysOfWeek.index(globalref. genOptions['WeekStart']) self.calendar.setFirstDayOfWeek(weekStart + 1) self.calendar.setVerticalHeaderFormat(QCalendarWidget. NoVerticalHeader) self.calendar.clicked.connect(self.contentsChanged) vertLayout = QVBoxLayout() horizLayout.addLayout(vertLayout) upperLayout = QHBoxLayout() vertLayout.addLayout(upperLayout) upperLayout.addStretch(0) self.hourBox = TimeSpinBox(TimeElem.hour, 1, 12, False) upperLayout.addWidget(self.hourBox) self.hourBox.valueChanged.connect(self.signalUpdate) self.hourBox.focusChanged.connect(self.handleFocusChange) colon = QLabel(':') upperLayout.addWidget(colon) self.minuteBox = TimeSpinBox(TimeElem.minute, 0, 59, True) upperLayout.addWidget(self.minuteBox) self.minuteBox.valueChanged.connect(self.signalUpdate) self.minuteBox.focusChanged.connect(self.handleFocusChange) colon = QLabel(':') upperLayout.addWidget(colon) self.secondBox = TimeSpinBox(TimeElem.second, 0, 59, True) upperLayout.addWidget(self.secondBox) self.secondBox.valueChanged.connect(self.signalUpdate) self.secondBox.focusChanged.connect(self.handleFocusChange) self.amPmBox = AmPmSpinBox() upperLayout.addSpacing(4) upperLayout.addWidget(self.amPmBox) self.amPmBox.valueChanged.connect(self.signalUpdate) upperLayout.addStretch(0) lowerLayout = QHBoxLayout() vertLayout.addLayout(lowerLayout) self.clock = ClockWidget() lowerLayout.addWidget(self.clock, Qt.AlignCenter) self.clock.numClicked.connect(self.setFromClock) if addCalendar: self.calendar.setFocus() self.updateClock() else: self.hourBox.setFocus() self.hourBox.selectAll() def setTimeFromText(self, text): """Set the time dialog from a string. Arguments: text -- the time in ISO format """ time = (datetime.datetime. strptime(text, fieldformat.TimeField.isoFormat).time()) hour = time.hour if time.hour <= 12 else time.hour - 12 self.blockSignals(True) self.hourBox.setValue(hour) self.minuteBox.setValue(time.minute) self.secondBox.setValue(time.second) amPm = 'AM' if time.hour < 12 else 'PM' self.amPmBox.setValue(amPm) self.blockSignals(False) self.updateClock() def setDateFromText(self, text): """Set the date dialog from a string. Arguments: text -- the date in ISO format """ date = QDate.fromString(text, Qt.ISODate) if date.isValid(): self.calendar.setSelectedDate(date) def timeObject(self): """Return a datetime time object for the current dialog setting. """ hour = self.hourBox.value() if self.amPmBox.value == 'PM': if hour < 12: hour += 12 elif hour == 12: hour = 0 return datetime.time(hour, self.minuteBox.value(), self.secondBox.value()) def updateClock(self): """Update the clock based on the current time and focused widget. """ hands = [self.focusElem] if self.focusElem else [TimeElem.hour, TimeElem.minute, TimeElem.second] self.clock.setDisplay(self.timeObject(), hands) def handleFocusChange(self, elemType, isFocused): """Update clock based on focus changes. Arguments: elemType -- the TimeElem of the focus change isFocused -- True if focus is gained """ if isFocused: if elemType != self.focusElem: self.focusElem = elemType self.updateClock() elif elemType == self.focusElem: self.focusElem = None self.updateClock() def setFromClock(self, num): """Set the active spin box value from a clock click. Arguments: num -- the number clicked """ spinBox = getattr(self, self.focusElem.name + 'Box') spinBox.setValue(num) spinBox.selectAll() def signalUpdate(self): """Signal a time change and update the clock. """ self.updateClock() self.contentsChanged.emit() class TimeSpinBox(QSpinBox): """A spin box for time values with optional leading zero. """ focusChanged = pyqtSignal(TimeElem, bool) def __init__(self, elemType, minValue, maxValue, leadZero=True, parent=None): """Initialize the spin box. Arguments: elemType -- the TimeElem of this box minValue -- the minimum allowed value maxValue -- the maximum allowed value leadZero -- true if a leading zero used with single digit values parent -- the box's parent widget """ self.elemType = elemType self.leadZero = leadZero super().__init__(parent) self.setMinimum(minValue) self.setMaximum(maxValue) self.setWrapping(True) self.setAlignment(Qt.AlignRight) def textFromValue(self, value): """Override to optionally add leading zero. Arguments: value -- the int value to convert """ if self.leadZero and value < 10: return '0' + repr(value) return repr(value) def focusInEvent(self, event): """Emit a signal when focused. Arguments: event -- the focus event """ super().focusInEvent(event) self.focusChanged.emit(self.elemType, True) def focusOutEvent(self, event): """Emit a signal if focus is lost. Arguments: event -- the focus event """ super().focusOutEvent(event) self.focusChanged.emit(self.elemType, False) class AmPmSpinBox(QAbstractSpinBox): """A spin box for AM/PM values. """ valueChanged = pyqtSignal() def __init__(self, parent=None): """Initialize the spin box. Arguments: parent -- the box's parent widget """ super().__init__(parent) self.value = 'AM' self.setDisplay() def stepBy(self, steps): """Step the spin box to the alternate value. Arguments: steps -- number of steps (ignored) """ self.value = 'PM' if self.value == 'AM' else 'AM' self.setDisplay() def stepEnabled(self): """Return enabled to show that stepping is always enabled. """ return (QAbstractSpinBox.StepUpEnabled | QAbstractSpinBox.StepDownEnabled) def setValue(self, value): """Set to text value if valid. Arguments: value -- the text value to set """ if value in ('AM', 'PM'): self.value = value self.setDisplay() def setDisplay(self): """Update display to match value. """ self.lineEdit().setText(self.value) self.valueChanged.emit() if self.hasFocus(): self.selectAll() def validate(self, inputStr, pos): """Check if the input string is acceptable. Arguments: inputStr -- the string to check pos -- the pos in the string (ignored) """ inputStr = inputStr.upper() if inputStr in ('AM', 'A'): self.value = 'AM' self.setDisplay() return (QValidator.Acceptable, 'AM', 2) if inputStr in ('PM', 'P'): self.value = 'PM' self.setDisplay() return (QValidator.Acceptable, 'PM', 2) return (QValidator.Invalid, 'xx', 2) def sizeHint(self): """Set prefered size. """ return super().sizeHint() + QSize(QFontMetrics(self.font()). width('AM'), 0) def focusInEvent(self, event): """Set select all when focused. Arguments: event -- the focus event """ super().focusInEvent(event) self.selectAll() def focusOutEvent(self, event): """Remove selection if focus is lost. Arguments: event -- the focus event """ super().focusOutEvent(event) self.lineEdit().deselect() class ClockWidget(QWidget): """A widget showing a clickable clock face. """ radius = 80 margin = 10 handLengths = {TimeElem.hour: int(radius * 0.5), TimeElem.minute: int(radius * 0.9), TimeElem.second: int(radius * 0.95)} handWidths = {TimeElem.hour: 7, TimeElem.minute: 5, TimeElem.second: 2} divisor = {TimeElem.hour: 120, TimeElem.minute: 10, TimeElem.second: 1 / 6} numClicked = pyqtSignal(int) def __init__(self, parent=None): """Initialize the clock. Arguments: parent -- the dialog's parent widget """ super().__init__(parent) self.time = datetime.time() self.hands = [] self.highlightAngle = None self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.setMouseTracking(True) def setDisplay(self, time, hands): """Set the clock display. Arguments: time -- a datetime time value hands -- a list of TimeElem clock hands to show """ self.time = time self.hands = hands self.highlightAngle = None self.update() def paintEvent(self, event): """Paint the clock face. Arguments: event -- the paint event """ painter = QPainter(self) painter.save() painter.setBrush(QApplication.palette().base()) painter.setPen(Qt.NoPen) painter.drawEllipse(self.rect()) painter.translate(ClockWidget.radius + ClockWidget.margin, ClockWidget.radius + ClockWidget.margin) for timeElem in self.hands: painter.save() painter.setBrush(QApplication.palette().windowText()) painter.setPen(Qt.NoPen) seconds = (self.time.hour * 3600 + self.time.minute * 60 + self.time.second) angle = seconds / ClockWidget.divisor[timeElem] % 360 if len(self.hands) == 1: painter.setBrush(QApplication.palette().highlight()) if self.hands[0] == TimeElem.hour: angle = int(angle // 30 * 30) # truncate to whole hour else: angle = int(angle // 6 * 6) # truncate to whole min/sec painter.rotate(angle) points = (QPoint(0, -ClockWidget.handLengths[timeElem]), QPoint(ClockWidget.handWidths[timeElem], 8), QPoint(-ClockWidget.handWidths[timeElem], 8)) painter.drawConvexPolygon(*points) painter.restore() rect = QRect(0, 0, 20, 20) if len(self.hands) != 1 or self.hands[0] == TimeElem.hour: labels = [repr(num) for num in range(1, 13)] else: labels = ['{0:0>2}'.format(num) for num in range(5, 56, 5)] labels.append('00') for ang in range(30, 361, 30): rect.moveCenter(self.pointOnRadius(ang)) painter.setPen(QPen()) if len(self.hands) == 1 and (ang == angle or ang == self.highlightAngle): painter.setPen(QPen(QApplication.palette().highlight(), 1)) painter.drawText(rect, Qt.AlignCenter, labels.pop(0)) painter.restore() super().paintEvent(event) def sizeHint(self): """Set prefered size. """ width = (ClockWidget.radius + ClockWidget.margin) * 2 return QSize(width, width) def pointOnRadius(self, angle): """Return a QPoint on the radius at the given angle. Arguments: angle -- the angle in dgrees from vertical (clockwise) """ angle = math.radians(angle) x = round(ClockWidget.radius * math.sin(angle)) y = 0 - round(ClockWidget.radius * math.cos(angle)) return QPoint(x, y) def pointToPosition(self, point): """Return a position (1 to 12) based on a screen point. Return None if not on a position. Arguments: point -- a QPoint screen position """ x = point.x() - ClockWidget.radius - ClockWidget.margin y = point.y() - ClockWidget.radius - ClockWidget.margin radius = math.sqrt(x**2 + y**2) if (ClockWidget.radius - 2 * ClockWidget.margin <= radius <= ClockWidget.radius + 2 * ClockWidget.margin): angle = math.degrees(math.atan2(-x, y)) + 180 if angle % 30 <= 10 or angle % 30 >= 20: pos = round(angle / 30) if pos == 0: pos = 12 return pos return None def mousePressEvent(self, event): """Signal user clicks on clock numbers if in single hand mode. Arguments: event -- the mouse press event """ if len(self.hands) == 1 and event.button() == Qt.LeftButton: pos = self.pointToPosition(event.pos()) if pos: if self.hands[0] != TimeElem.hour: if pos == 12: pos = 0 pos *= 5 self.numClicked.emit(pos) super().mousePressEvent(event) def mouseMoveEvent(self, event): """Highlight clickable numbers if in single hand mode. Arguments: event -- the mouse move event """ if len(self.hands) == 1: pos = self.pointToPosition(event.pos()) if pos: self.highlightAngle = pos * 30 self.update() elif self.highlightAngle != None: self.highlightAngle = None self.update() super().mouseMoveEvent(event) class DateTimeEditor(ComboEditor): """An editor widget for DateTimeFields. Uses a combo box with a clandar widget in place of the list popup. """ def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.dialog = None nowAction = QAction(_('Set to &Now'), self) nowAction.triggered.connect(self.setNow) self.lineEdit().extraMenuActions = [nowAction] def showPopup(self): """Override to show a popup entry widget in place of a list view. """ if not self.dialog: self.dialog = TimeDialog(True, self) self.dialog.contentsChanged.connect(self.setDateTime) self.dialog.show() pos = self.mapToGlobal(self.rect().bottomRight()) pos.setX(pos.x() - self.dialog.width() + 1) screenBottom = QApplication.desktop().screenGeometry(self).bottom() if pos.y() + self.dialog.height() > screenBottom: pos.setY(pos.y() - self.rect().height() - self.dialog.height()) self.dialog.move(pos) try: storedText = self.fieldRef.storedText(self.currentText()) except ValueError: storedText = '' if storedText: dateText, timeText = storedText.split(' ', 1) self.dialog.setDateFromText(dateText) self.dialog.setTimeFromText(timeText) def hidePopup(self): """Override to hide the popup entry widget. """ if self.dialog: self.dialog.hide() super().hidePopup() def setDateTime(self): """Set the date and time based on a signal from the dialog calendar. """ if self.dialog: dateStr = self.dialog.calendar.selectedDate().toString(Qt.ISODate) timeStr = self.dialog.timeObject().isoformat() + '.000' self.setEditText(self.fieldRef.formatEditorText(dateStr + ' ' + timeStr)) def setNow(self): """Set to the current date and time. """ dateTime = QDateTime.currentDateTime() dateTimeStr = dateTime.toString('yyyy-MM-dd HH:mm:ss.zzz') self.setEditText(self.fieldRef.formatEditorText(dateTimeStr)) class ExtLinkEditor(ComboEditor): """An editor widget for external link fields. Uses a combo box with a link entry box in place of the list popup. """ dragLinkEnabled = True def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.setAcceptDrops(True) self.dialog = None openAction = QAction(_('&Open Link'), self) openAction.triggered.connect(self.openLink) folderAction = QAction(_('Open &Folder'), self) folderAction.triggered.connect(self.openFolder) self.lineEdit().extraMenuActions = [openAction, folderAction] self.lineEdit().contextMenuPrep.connect(self.updateActions) def showPopup(self): """Override to show a popup entry widget in place of a list view. """ if not self.dialog: self.dialog = ExtLinkDialog(True, self) self.dialog.contentsChanged.connect(self.setLink) self.dialog.show() pos = self.mapToGlobal(self.rect().bottomRight()) pos.setX(pos.x() - self.dialog.width() + 1) screenBottom = QApplication.desktop().screenGeometry(self).bottom() if pos.y() + self.dialog.height() > screenBottom: pos.setY(pos.y() - self.rect().height() - self.dialog.height()) self.dialog.move(pos) self.dialog.setFromEditor(self.currentText()) def hidePopup(self): """Override to hide the popup entry widget. """ if self.dialog: self.dialog.hide() super().hidePopup() def setLink(self): """Set the current link from the popup dialog. """ self.setEditText(self.dialog.editorText()) def openLink(self): """Open the link in a web browser. """ text = self.currentText() if text: nameMatch = fieldformat.linkSeparateNameRegExp.match(text) if nameMatch: address = nameMatch.group(1).strip() else: address = text.strip() if address: if urltools.isRelative(address): defaultPath = globalref.mainControl.defaultPathObj(True) address = urltools.toAbsolute(address, str(defaultPath)) openExtUrl(address) def openFolder(self): """Open the link in a file manager/explorer. """ text = self.currentText() if text: nameMatch = fieldformat.linkSeparateNameRegExp.match(text) if nameMatch: address = nameMatch.group(1).strip() else: address = text.strip() if address and urltools.extractScheme(address) in ('', 'file'): if urltools.isRelative(address): defaultPath = globalref.mainControl.defaultPathObj(True) address = urltools.toAbsolute(address, str(defaultPath)) address = os.path.dirname(address) openExtUrl(address) def updateActions(self): """Set availability of custom context menu actions. """ address = self.currentText() if address: nameMatch = fieldformat.linkSeparateNameRegExp.match(address) if nameMatch: address = nameMatch.group(1).strip() else: address = address.strip() openAction, folderAction = self.lineEdit().extraMenuActions openAction.setEnabled(len(address) > 0) folderAction.setEnabled(len(address) > 0 and urltools.extractScheme(address) in ('', 'file')) def addDroppedUrl(self, urlText): """Add the URL link that was dropped on this editor from the view. Arguments: urlText -- the text of the link """ self.setEditText(urlText) def dragEnterEvent(self, event): """Accept drags of files to this widget. Arguments: event -- the drag event object """ if event.mimeData().hasUrls(): event.accept() def dropEvent(self, event): """Open a file dropped onto this widget. Arguments: event -- the drop event object """ fileList = event.mimeData().urls() if fileList: self.setEditText(fileList[0].toLocalFile()) _extLinkSchemes = ('http://', 'https://', 'mailto:', 'file://') _extLinkSchemeDict = {proto.split(':', 1)[0]: proto for proto in _extLinkSchemes} class ExtLinkDialog(QDialog): """A popup or normal dialog box for external link editing. """ contentsChanged = pyqtSignal() def __init__(self, popupDialog=False, parent=None): """Initialize the dialog widgets. Arguments: popupDialog -- add OK and cancel buttons if False parent -- the dialog's parent widget """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('External Link')) vertLayout = QVBoxLayout(self) vertLayout.setSpacing(1) schemeLabel = QLabel(_('Scheme')) vertLayout.addWidget(schemeLabel) schemeLayout = QHBoxLayout() vertLayout.addLayout(schemeLayout) schemeLayout.setSpacing(8) self.schemeButtons = QButtonGroup(self) self.schemeButtonDict = {} for scheme in _extLinkSchemes: scheme = scheme.split(':', 1)[0] button = QRadioButton(scheme) self.schemeButtons.addButton(button) self.schemeButtonDict[scheme] = button schemeLayout.addWidget(button) self.schemeButtonDict['http'].setChecked(True) self.schemeButtons.buttonClicked.connect(self.updateScheme) vertLayout.addSpacing(8) self.browseButton = QPushButton(_('&Browse for File')) self.browseButton.setAutoDefault(False) self.browseButton.clicked.connect(self.fileBrowse) vertLayout.addWidget(self.browseButton) vertLayout.addSpacing(8) self.pathTypeLabel = QLabel(_('File Path Type')) vertLayout.addWidget(self.pathTypeLabel) pathTypeLayout = QHBoxLayout() vertLayout.addLayout(pathTypeLayout) pathTypeLayout.setSpacing(8) pathTypeButtons = QButtonGroup(self) self.absoluteButton = QRadioButton(_('Absolute')) pathTypeButtons.addButton(self.absoluteButton) pathTypeLayout.addWidget(self.absoluteButton) self.relativeButton = QRadioButton(_('Relative')) pathTypeButtons.addButton(self.relativeButton) pathTypeLayout.addWidget(self.relativeButton) self.absoluteButton.setChecked(True) pathTypeButtons.buttonClicked.connect(self.updatePathType) vertLayout.addSpacing(8) addressLabel = QLabel(_('Address')) vertLayout.addWidget(addressLabel) self.addressEdit = QLineEdit() self.addressEdit.textEdited.connect(self.checkAddress) vertLayout.addWidget(self.addressEdit) vertLayout.addSpacing(8) nameLabel = QLabel(_('Display Name')) vertLayout.addWidget(nameLabel) self.nameEdit = QLineEdit() self.nameEdit.textEdited.connect(self.contentsChanged) vertLayout.addWidget(self.nameEdit) if popupDialog: self.setWindowFlags(Qt.Popup) else: vertLayout.addSpacing(8) ctrlLayout = QHBoxLayout() vertLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.addressEdit.setFocus() def setFromEditor(self, editorText): """Set the dialog contents from a string in editor format. Arguments: editorText -- string in "link [name]" format """ name = address = '' editorText = editorText.strip() if editorText: nameMatch = fieldformat.linkSeparateNameRegExp.match(editorText) if nameMatch: address, name = nameMatch.groups() address = address.strip() else: address = editorText name = urltools.shortName(address) self.setFromComponents(address, name) def setFromComponents(self, address, name): """Set the dialog contents from separate address and name. Arguments: address -- the link address, including the scheme prefix name -- the displayed name for the link """ scheme = urltools.extractScheme(address) if scheme not in _extLinkSchemeDict: if not scheme: address = urltools.replaceScheme('file', address) scheme = 'file' self.schemeButtonDict[scheme].setChecked(True) if address and urltools.isRelative(address): self.relativeButton.setChecked(True) else: self.absoluteButton.setChecked(True) self.addressEdit.setText(address) self.nameEdit.setText(name) self.updateFileControls() def editorText(self): """Return the dialog contents in data editor format ("link [name]"). """ address = self.currentAddress() if not address: return '' name = self.nameEdit.text().strip() if not name: name = urltools.shortName(address) return '{0} [{1}]'.format(address, name) def htmlText(self): """Return the dialog contents in HTML link format. """ address = self.currentAddress() if not address: return '' name = self.nameEdit.text().strip() if not name: name = urltools.shortName(address) return '{1}'.format(address, name) def currentAddress(self): """Return current address with the selected scheme prefix. """ scheme = self.schemeButtons.checkedButton().text() address = self.addressEdit.text().strip() return urltools.replaceScheme(scheme, address) def checkAddress(self): """Update controls based on a change to the address field. Makes minimum changes to scheme and absolute controls, since the address may be incomplete. """ address = self.addressEdit.text().strip() scheme = urltools.extractScheme(address) if scheme in _extLinkSchemeDict: self.schemeButtonDict[scheme].setChecked(True) if scheme != 'file': self.absoluteButton.setChecked(True) self.updateFileControls() self.contentsChanged.emit() def updateScheme(self): """Update scheme in the address due to scheme button change. """ scheme = self.schemeButtons.checkedButton().text() address = self.addressEdit.text().strip() address = urltools.replaceScheme(scheme, address) self.addressEdit.setText(address) if urltools.isRelative(address): self.relativeButton.setChecked(True) else: self.absoluteButton.setChecked(True) self.updateFileControls() self.contentsChanged.emit() def updatePathType(self): """Update file path based on a change in the absolute/relative control. """ absolute = self.absoluteButton.isChecked() defaultPath = globalref.mainControl.defaultPathObj(True) address = self.addressEdit.text().strip() if absolute: address = urltools.toAbsolute(address, str(defaultPath)) else: address = urltools.toRelative(address, str(defaultPath)) self.addressEdit.setText(address) self.contentsChanged.emit() def updateFileControls(self): """Set file browse & type controls available based on current scheme. """ enable = self.schemeButtons.checkedButton().text() == 'file' self.browseButton.setEnabled(enable) self.pathTypeLabel.setEnabled(enable) self.absoluteButton.setEnabled(enable) self.relativeButton.setEnabled(enable) def fileBrowse(self): """Show dialog to browse for a file to be linked. Adjust based on absolute or relative path settings. """ refPath = str(globalref.mainControl.defaultPathObj(True)) defaultPath = refPath oldAddress = self.addressEdit.text().strip() oldScheme = urltools.extractScheme(oldAddress) if oldAddress and not oldScheme or oldScheme == 'file': if urltools.isRelative(oldAddress): oldAddress = urltools.toAbsolute(oldAddress, refPath) oldAddress = urltools.extractAddress(oldAddress) if os.access(oldAddress, os.F_OK): defaultPath = oldAddress address, selFltr = QFileDialog.getOpenFileName(self, _('TreeLine - External Link File'), defaultPath, globalref.fileFilters['all']) if address: if self.relativeButton.isChecked(): address = urltools.toRelative(address, refPath) self.setFromComponents(address, urltools.shortName(address)) self.show() self.contentsChanged.emit() class IntLinkEditor(ComboEditor): """An editor widget for internal link fields. Uses a combo box with a link select dialog in place of the list popup. """ inLinkSelectMode = pyqtSignal(bool) def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.address = '' self.intLinkDialog = None self.setLineEdit(PartialLineEditor(self)) openAction = QAction(_('&Go to Target'), self) openAction.triggered.connect(self.openLink) clearAction = QAction(_('Clear &Link'), self) clearAction.triggered.connect(self.clearLink) self.lineEdit().extraMenuActions = [openAction, clearAction] def setContents(self, text): """Set the contents of the editor to text. Arguments: text - the new text contents for the editor """ super().setContents(text) if not text: self.lineEdit().staticLength = 0 self.address = '' return try: self.address, name = self.fieldRef.addressAndName(self.nodeRef. data.get(self.fieldRef.name, '')) except ValueError: self.address = '' self.address = self.address.lstrip('#') nameMatch = fieldformat.linkSeparateNameRegExp.match(text) if nameMatch: link = nameMatch.group(1) self.lineEdit().staticLength = len(link) + 1 else: self.lineEdit().staticLength = 0 def contents(self): """Return the editor contents in "address [name]" format. """ if not self.address: return self.currentText() nameMatch = fieldformat.linkSeparateNameRegExp.match(self. currentText()) if nameMatch: name = nameMatch.group(2) else: name = '' return '{0} [{1}]'.format(self.address, name.strip()) def clearLink(self): """Clear the contents of the editor. """ self.setContents('') self.signalUpdate() def showPopup(self): """Override to show a popup entry widget in place of a list view. """ if not self.intLinkDialog: self.intLinkDialog = IntLinkDialog(True, self) self.intLinkDialog.show() pos = self.mapToGlobal(self.rect().bottomRight()) pos.setX(pos.x() - self.intLinkDialog.width() + 1) screenBottom = (QApplication.desktop().screenGeometry(self). bottom()) if pos.y() + self.intLinkDialog.height() > screenBottom: pos.setY(pos.y() - self.rect().height() - self.intLinkDialog.height()) self.intLinkDialog.move(pos) self.inLinkSelectMode.emit(True) def hidePopup(self): """Override to hide the popup entry widget. """ if self.intLinkDialog: self.intLinkDialog.hide() self.inLinkSelectMode.emit(False) super().hidePopup() def setLinkFromNode(self, node): """Set the current link from a clicked node. Arguments: node -- the node to set the unique ID from """ self.hidePopup() self.address = node.uId linkTitle = node.title() nameMatch = fieldformat.linkSeparateNameRegExp.match(self. currentText()) if nameMatch: name = nameMatch.group(2) else: name = linkTitle self.setEditText('LinkTo: {0} [{1}]'.format(linkTitle, name)) self.lineEdit().staticLength = len(linkTitle) + 9 def openLink(self): """Open the link in a web browser. """ if self.address: editView = self.parent().parent() editView.treeView.selectionModel().selectNodeById(self.address) def setCursorPoint(self, point): """Set the cursor to the given point. Arguments: point -- the QPoint for the new cursor position """ self.lineEdit().setCursorPoint(point) self.lineEdit().fixSelection() class PartialLineEditor(LineEditor): """A line used in internal link combo editors. Only allows the name portion to be selected or editd. """ def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent, True) self.staticLength = 0 def fixSelection(self): """Fix the selection and cursor to not include static portion of text. """ cursorPos = self.cursorPosition() if -1 < self.selectionStart() < self.staticLength: endPos = self.selectionStart() + len(self.selectedText()) if endPos > self.staticLength: if cursorPos >= self.staticLength: self.setSelection(self.staticLength, endPos - self.staticLength) else: # reverse select to get cursor at selection start self.setSelection(endPos, self.staticLength - endPos) return self.deselect() if cursorPos < self.staticLength: self.setCursorPosition(self.staticLength) def selectAll(self): """Select all editable text. """ self.setSelection(self.staticLength, len(self.text())) def mouseReleaseEvent(self, event): """Fix selection if required after mouse release. Arguments: event -- the mouse release event """ super().mouseReleaseEvent(event) self.fixSelection() def keyPressEvent(self, event): """Avoid edits or cursor movements to the static portion of the text. Arguments: event -- the mouse release event """ if (event.key() == Qt.Key_Backspace and (self.cursorPosition() <= self.staticLength and not self.hasSelectedText())): return if event.key() in (Qt.Key_Left, Qt.Key_Home): super().keyPressEvent(event) self.fixSelection() return super().keyPressEvent(event) class IntLinkDialog(QDialog): """A popup dialog box for internal link editing. """ contentsChanged = pyqtSignal() def __init__(self, popupDialog=False, parent=None): """Initialize the dialog widgets. Arguments: popupDialog -- add OK and cancel buttons if False parent -- the dialog's parent widget """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) layout = QVBoxLayout(self) label = QLabel(_('(Click link target in tree)')) layout.addWidget(label) class EmbedIntLinkDialog(QDialog): """A popup or normal dialog box for internal link editing. """ contentsChanged = pyqtSignal() targetClickDialogRef = None def __init__(self, structRef, parent=None): """Initialize the dialog widgets. Arguments: structRef -- a ref to the tree structure parent -- the dialog's parent widget """ super().__init__(parent) self.structRef = structRef self.address = '' self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Internal Link')) vertLayout = QVBoxLayout(self) vertLayout.setSpacing(1) self.linkLabel = QLabel() vertLayout.addWidget(self.linkLabel) infoLabel = QLabel(_('(Click link target in tree)')) vertLayout.addWidget(infoLabel) vertLayout.addSpacing(8) nameLabel = QLabel(_('Display Name')) vertLayout.addWidget(nameLabel) self.nameEdit = QLineEdit() self.nameEdit.textEdited.connect(self.contentsChanged) vertLayout.addWidget(self.nameEdit) vertLayout.addSpacing(8) ctrlLayout = QHBoxLayout() vertLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) self.okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.setDefault(True) self.okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) def updateLinkText(self): """Update the link label using the current address. """ title = '' name = self.nameEdit.text().strip() if self.address: targetNode = self.structRef.nodeDict.get(self.address, None) if targetNode: title = targetNode.title() if not name: self.nameEdit.setText(title) self.linkLabel.setText('LinkTo: {0}'.format(title)) self.okButton.setEnabled(len(self.address) > 0) def setFromNode(self, node): """Set the dialog contents from a clicked node. Arguments: node -- the node to set the unique ID from """ self.address = node.uId self.updateLinkText() def setFromComponents(self, address, name): """Set the dialog contents from separate address and name. Arguments: address -- the link address, including the protocol prefix name -- the displayed name for the link """ self.address = address self.nameEdit.setText(name) self.updateLinkText() def htmlText(self): """Return the dialog contents in HTML link format. """ name = self.nameEdit.text().strip() if not name: name = _('link') return '{1}'.format(self.address, name) class PictureLinkEditor(ComboEditor): """An editor widget for picture link fields. Uses a combo box with a link entry box in place of the list popup. """ dragLinkEnabled = True def __init__(self, parent=None): """Initialize the editor class. Arguments: parent -- the parent, if given """ super().__init__(parent) self.dialog = None openAction = QAction(_('&Open Picture'), self) openAction.triggered.connect(self.openPicture) self.lineEdit().extraMenuActions = [openAction] def showPopup(self): """Override to show a popup entry widget in place of a list view. """ if not self.dialog: self.dialog = PictureLinkDialog(True, self) self.dialog.contentsChanged.connect(self.setLink) self.dialog.show() pos = self.mapToGlobal(self.rect().bottomRight()) pos.setX(pos.x() - self.dialog.width() + 1) screenBottom = (QApplication.desktop().screenGeometry(self). bottom()) if pos.y() + self.dialog.height() > screenBottom: pos.setY(pos.y() - self.rect().height() - self.dialog.height()) self.dialog.move(pos) self.dialog.setAddress(self.currentText()) def hidePopup(self): """Override to hide the popup entry widget. """ if self.dialog: self.dialog.hide() super().hidePopup() def setLink(self): """Set the current link from the popup dialog. """ self.setEditText(self.dialog.currentAddress()) def openPicture(self): """Open the link in a web browser. """ address = self.currentText() if address: if urltools.isRelative(address): defaultPath = globalref.mainControl.defaultPathObj(True) address = urltools.toAbsolute(address, str(defaultPath)) openExtUrl(address) def addDroppedUrl(self, urlText): """Add the URL link that was dropped on this editor from the view. Arguments: urlText -- the text of the link """ self.setEditText(urlText) class PictureLinkDialog(QDialog): """A popup or normal dialog box for picture link editing. """ thumbnailSize = QSize(250, 100) contentsChanged = pyqtSignal() def __init__(self, popupDialog=False, parent=None): """Initialize the dialog widgets. Arguments: popupDialog -- add OK and cancel buttons if False parent -- the dialog's parent widget """ super().__init__(parent) self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) self.setWindowTitle(_('Picture Link')) self.setMinimumWidth(self.thumbnailSize.width()) vertLayout = QVBoxLayout(self) vertLayout.setSpacing(1) self.thumbnail = QLabel() pixmap = QPixmap(self.thumbnailSize) pixmap.fill() self.thumbnail.setPixmap(pixmap) vertLayout.addWidget(self.thumbnail, 0, Qt.AlignHCenter) vertLayout.addSpacing(8) self.browseButton = QPushButton(_('&Browse for File')) self.browseButton.setAutoDefault(False) self.browseButton.clicked.connect(self.fileBrowse) vertLayout.addWidget(self.browseButton) vertLayout.addSpacing(8) self.pathTypeLabel = QLabel(_('File Path Type')) vertLayout.addWidget(self.pathTypeLabel) pathTypeLayout = QHBoxLayout() vertLayout.addLayout(pathTypeLayout) pathTypeLayout.setSpacing(8) pathTypeButtons = QButtonGroup(self) self.absoluteButton = QRadioButton(_('Absolute')) pathTypeButtons.addButton(self.absoluteButton) pathTypeLayout.addWidget(self.absoluteButton) self.relativeButton = QRadioButton(_('Relative')) pathTypeButtons.addButton(self.relativeButton) pathTypeLayout.addWidget(self.relativeButton) self.absoluteButton.setChecked(True) pathTypeButtons.buttonClicked.connect(self.updatePathType) vertLayout.addSpacing(8) addressLabel = QLabel(_('Address')) vertLayout.addWidget(addressLabel) self.addressEdit = QLineEdit() self.addressEdit.textEdited.connect(self.checkAddress) vertLayout.addWidget(self.addressEdit) vertLayout.addSpacing(8) if popupDialog: self.setWindowFlags(Qt.Popup) else: vertLayout.addSpacing(8) ctrlLayout = QHBoxLayout() vertLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) okButton.clicked.connect(self.accept) cancelButton = QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) cancelButton.clicked.connect(self.reject) self.addressEdit.setFocus() def setAddress(self, address): """Set the dialog contents from a string in editor format. Arguments: address -- URL string for the address """ if address and urltools.isRelative(address): self.relativeButton.setChecked(True) else: self.absoluteButton.setChecked(True) self.addressEdit.setText(address) self.updateThumbnail() def setFromHtml(self, htmlStr): """Set the dialog contents from an HTML link. Arguments: htmlStr -- string in HTML link format """ linkMatch = imageRegExp.search(htmlStr) if linkMatch: address = linkMatch.group(1) self.setAddress(address.strip()) def htmlText(self): """Return the dialog contents in HTML link format. """ address = self.currentAddress() if not address: return '' return ''.format(address) def currentAddress(self): """Return current address with the selected scheme prefix. """ return self.addressEdit.text().strip() def checkAddress(self): """Update absolute controls based on a change to the address field. """ address = self.addressEdit.text().strip() if address: if urltools.isRelative(address): self.relativeButton.setChecked(True) else: self.absoluteButton.setChecked(True) self.updateThumbnail() self.contentsChanged.emit() def updatePathType(self): """Update path based on a change in the absolute/relative control. """ absolute = self.absoluteButton.isChecked() defaultPath = globalref.mainControl.defaultPathObj(True) address = self.addressEdit.text().strip() if absolute: address = urltools.toAbsolute(address, str(defaultPath), False) else: address = urltools.toRelative(address, str(defaultPath)) self.addressEdit.setText(address) self.updateThumbnail() self.contentsChanged.emit() def updateThumbnail(self): """Update the thumbnail with an image from the current address. """ address = self.addressEdit.text().strip() if urltools.isRelative(address): refPath = str(globalref.mainControl.defaultPathObj(True)) address = urltools.toAbsolute(address, refPath, False) pixmap = QPixmap(address) if pixmap.isNull(): pixmap = QPixmap(self.thumbnailSize) pixmap.fill() else: pixmap = pixmap.scaled(self.thumbnailSize, Qt.KeepAspectRatio) self.thumbnail.setPixmap(pixmap) def fileBrowse(self): """Show dialog to browse for a file to be linked. Adjust based on absolute or relative path settings. """ refPath = str(globalref.mainControl.defaultPathObj(True)) defaultPath = refPath oldAddress = self.addressEdit.text().strip() if oldAddress: if urltools.isRelative(oldAddress): oldAddress = urltools.toAbsolute(oldAddress, refPath) oldAddress = urltools.extractAddress(oldAddress) if os.access(oldAddress, os.F_OK): defaultPath = oldAddress address, selFltr = QFileDialog.getOpenFileName(self, _('TreeLine - Picture File'), defaultPath, globalref.fileFilters['all']) if address: if self.relativeButton.isChecked(): address = urltools.toRelative(address, refPath) self.setAddress(address) self.updateThumbnail() self.show() self.contentsChanged.emit() #### Utility Functions #### def openExtUrl(path): """Open a web browser or a application for a directory or file. Arguments: path -- the path to open """ if sys.platform.startswith('win'): os.startfile(path) elif sys.platform.startswith('darwin'): subprocess.call(['open', path]) else: subprocess.call(['xdg-open', path]) TreeLine/source/treelocalcontrol.py0000644000175000017500000022727513714637332016520 0ustar dougdoug#!/usr/bin/env python3 #****************************************************************************** # treelocalcontrol.py, provides a class for the main tree commands # # TreeLine, an information storage program # Copyright (C) 2020, 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 WITHOUT ANY WARRANTY. See the included LICENSE file for details. #****************************************************************************** import pathlib import json import os import sys import gzip import operator from itertools import chain from PyQt5.QtCore import QObject, QTimer, Qt, pyqtSignal from PyQt5.QtWidgets import (QAction, QActionGroup, QApplication, QDialog, QFileDialog, QMenu, QMessageBox) import treemaincontrol import treestructure import treemodel import treeformats import treenode import treewindow import exports import miscdialogs import printdata import matheval import spellcheck import undo import p3 import globalref class TreeLocalControl(QObject): """Class to handle controls local to a model/view combination. Provides methods for all local controls and stores a model & windows. """ controlActivated = pyqtSignal(QObject) controlClosed = pyqtSignal(QObject) def __init__(self, allActions, fileObj=None, treeStruct=None, forceNewWindow=False, parent=None): """Initialize the local tree controls. Use an imported structure if given or open the file if path is given. Always creates a new window. Arguments: allActions -- a dict containing the upper level actions fileObj -- the path object or file object to open, if given treeStruct -- an imported tree structure file, if given forceNewWindow -- if True, use a new window regardless of option parent -- a parent object if given """ super().__init__(parent) self.printData = printdata.PrintData(self) self.spellCheckLang = '' self.allActions = allActions.copy() self.setupActions() self.filePathObj = (pathlib.Path(fileObj.name) if hasattr(fileObj, 'read') else fileObj) if treeStruct: self.structure = treeStruct elif fileObj: if hasattr(fileObj, 'read'): fileData = json.load(fileObj) else: with fileObj.open('r', encoding='utf-8') as f: fileData = json.load(f) self.structure = treestructure.TreeStructure(fileData) self.printData.readData(fileData['properties']) self.spellCheckLang = fileData['properties'].get('spellchk', '') else: self.structure = treestructure.TreeStructure(addDefaults=True) fileInfoFormat = self.structure.treeFormats.fileInfoFormat fileInfoFormat.updateFileInfo(self.filePathObj, self.structure.fileInfoNode) self.model = treemodel.TreeModel(self.structure) self.model.treeModified.connect(self.updateRightViews) self.modified = False self.imported = False self.compressed = False self.encrypted = False self.windowList = [] self.activeWindow = None self.findReplaceSpotRef = (None, 0) QApplication.clipboard().dataChanged.connect(self.updateCommandsAvail) self.structure.undoList = undo.UndoRedoList(self. allActions['EditUndo'], self) self.structure.redoList = undo.UndoRedoList(self. allActions['EditRedo'], self) self.structure.undoList.altListRef = self.structure.redoList self.structure.redoList.altListRef = self.structure.undoList self.autoSaveTimer = QTimer(self) self.autoSaveTimer.timeout.connect(self.autoSave) if not globalref.mainControl.activeControl: self.windowNew(offset=0) elif forceNewWindow or globalref.genOptions['OpenNewWindow']: self.windowNew() else: oldControl = globalref.mainControl.activeControl window = oldControl.activeWindow if len(oldControl.windowList) > 1: oldControl.windowList.remove(window) else: oldControl.controlClosed.emit(oldControl) window.resetTreeModel(self.model) self.setWindowSignals(window, True) window.updateActions(self.allActions) self.windowList.append(window) self.updateWindowCaptions() self.activeWindow = window if fileObj and self.structure.childRefErrorNodes: msg = _('Warning - file corruption!\n' 'Skipped bad child references in the following nodes:') for node in self.structure.childRefErrorNodes: msg += '\n "{}"'.format(node.title()) QMessageBox.warning(self.activeWindow, 'TreeLine', msg) self.structure.childRefErrorNodes = [] def setWindowSignals(self, window, removeOld=False): """Setup signals between the window and this controller. Arguments: window -- the window to link removeOld -- if True, remove old signals """ if removeOld: window.selectChanged.disconnect() window.nodeModified.disconnect() window.treeModified.disconnect() window.winActivated.disconnect() window.winClosing.disconnect() window.selectChanged.connect(self.updateCommandsAvail) window.nodeModified.connect(self.updateTreeNode) window.treeModified.connect(self.updateTree) window.winActivated.connect(self.setActiveWin) window.winClosing.connect(self.checkWindowClose) window.setExternalSignals() def updateTreeNode(self, node, setModified=True): """Update the full tree in all windows. Also update right views in secondary windows. Arguments: node -- the node to be updated setModified -- if True, set the modified flag for this file """ if node.setConditionalType(self.structure): self.activeWindow.updateRightViews(outputOnly=True) if (self.structure.treeFormats.mathFieldRefDict and node.updateNodeMathFields(self.structure.treeFormats)): self.activeWindow.updateRightViews(outputOnly=True) if globalref.genOptions['ShowMath']: self.activeWindow.refreshDataEditViews() for window in self.windowList: window.updateTreeNode(node) if window.treeFilterView: window.treeFilterView.updateItem(node) if setModified: self.setModified() def updateTree(self, setModified=True): """Update the full tree in all windows. Also update right views in secondary windows. Arguments: setModified -- if True, set the modified flag for this file """ QApplication.setOverrideCursor(Qt.WaitCursor) typeChanges = 0 if self.structure.treeFormats.conditionalTypes: for node in self.structure.childList: typeChanges += node.setDescendantConditionalTypes(self. structure) self.updateAllMathFields() for window in self.windowList: window.updateTree() if window != self.activeWindow or typeChanges: window.updateRightViews() if window.treeFilterView: window.treeFilterView.updateContents() if setModified: self.setModified() QApplication.restoreOverrideCursor() def updateRightViews(self, setModified=False, otherTrees=False): """Update the right-hand views in all windows. Arguments: setModified -- if True, set the modified flag for this file otherTrees -- if True, also update trees in non-active windows """ for window in self.windowList: window.updateRightViews() if otherTrees and window != self.activeWindow: window.updateTree() if setModified: self.setModified() def updateAll(self, setModified=True): """Update the full tree and right-hand views in all windows. Arguments: setModified -- if True, set the modified flag for this file """ QApplication.setOverrideCursor(Qt.WaitCursor) if self.structure.treeFormats.conditionalTypes: for node in self.structure.childList: node.setDescendantConditionalTypes(self.structure) self.updateAllMathFields() for window in self.windowList: window.updateTree() if window.treeFilterView: window.treeFilterView.updateContents() window.updateRightViews() self.updateCommandsAvail() if setModified: self.setModified() # self.structure.debugCheck() QApplication.restoreOverrideCursor() def updateAllMathFields(self): """Recalculate all math fields in the entire tree. """ for eqnRefDict in self.structure.treeFormats.mathLevelList: if list(eqnRefDict.values())[0][0].evalDirection != (matheval. EvalDir. upward): for node in self.structure.descendantGen(): for eqnRef in eqnRefDict.get(node.formatRef.name, []): node.data[eqnRef.eqnField.name] = (eqnRef.eqnField. equationValue(node)) else: spot = self.structure.structSpot().lastDescendantSpot() while spot: node = spot.nodeRef for eqnRef in eqnRefDict.get(node.formatRef.name, []): node.data[eqnRef.eqnField.name] = (eqnRef.eqnField. equationValue(node)) spot = spot.prevTreeSpot() def updateCommandsAvail(self): """Set commands available based on node selections. """ selSpots = self.currentSelectionModel().selectedSpots() hasSelect = len(selSpots) > 0 rootSpots = [spot for spot in selSpots if not spot.parentSpot.parentSpot] hasPrevSibling = (len(selSpots) and None not in [spot.prevSiblingSpot() for spot in selSpots]) hasNextSibling = (len(selSpots) and None not in [spot.nextSiblingSpot() for spot in selSpots]) hasChildren = (sum([len(spot.nodeRef.childList) for spot in selSpots]) > 0) mime = QApplication.clipboard().mimeData() hasData = len(mime.data('application/json')) > 0 hasText = len(mime.data('text/plain')) > 0 self.allActions['EditPaste'].setEnabled(hasData or hasText) self.allActions['EditPasteChild'].setEnabled(hasData) self.allActions['EditPasteBefore'].setEnabled(hasData and hasSelect) self.allActions['EditPasteAfter'].setEnabled(hasData and hasSelect) self.allActions['EditPasteCloneChild'].setEnabled(hasData) self.allActions['EditPasteCloneBefore'].setEnabled(hasData and hasSelect) self.allActions['EditPasteCloneAfter'].setEnabled(hasData and hasSelect) self.allActions['NodeRename'].setEnabled(len(selSpots) == 1) self.allActions['NodeInsertBefore'].setEnabled(hasSelect) self.allActions['NodeInsertAfter'].setEnabled(hasSelect) self.allActions['NodeDelete'].setEnabled(hasSelect and len(rootSpots) < len(self.structure.childList)) self.allActions['NodeIndent'].setEnabled(hasPrevSibling) self.allActions['NodeUnindent'].setEnabled(hasSelect and len(rootSpots) == 0) self.allActions['NodeMoveUp'].setEnabled(hasPrevSibling) self.allActions['NodeMoveDown'].setEnabled(hasNextSibling) self.allActions['NodeMoveFirst'].setEnabled(hasPrevSibling) self.allActions['NodeMoveLast'].setEnabled(hasNextSibling) self.allActions['DataNodeType'].parent().setEnabled(hasSelect) self.allActions['DataFlatCategory'].setEnabled(hasChildren) self.allActions['DataAddCategory'].setEnabled(hasChildren) self.allActions['DataSwapCategory'].setEnabled(hasChildren) if self.activeWindow.treeFilterView: self.allActions['NodeInsertBefore'].setEnabled(False) self.allActions['NodeInsertAfter'].setEnabled(False) self.allActions['NodeAddChild'].setEnabled(False) self.allActions['NodeIndent'].setEnabled(False) self.allActions['NodeUnindent'].setEnabled(False) self.allActions['NodeMoveUp'].setEnabled(False) self.allActions['NodeMoveDown'].setEnabled(False) self.allActions['NodeMoveFirst'].setEnabled(False) self.allActions['NodeMoveLast'].setEnabled(False) else: self.allActions['NodeAddChild'].setEnabled(True) self.activeWindow.updateCommandsAvail() def updateWindowCaptions(self): """Update the caption for all windows. """ for window in self.windowList: window.setCaption(self.filePathObj, self.modified) def setModified(self, modified=True): """Set the modified flag on this file and update commands available. Arguments: modified -- the modified state to set """ if modified != self.modified: self.modified = modified self.allActions['FileSave'].setEnabled(modified) self.updateWindowCaptions() self.resetAutoSave() def expandRootNodes(self, maxNum=5): """Expand root node if there are fewer than the maximum. Arguments: maxNum -- only expand if there are fewer root nodes than this. """ if len(self.structure.childList) < maxNum: treeView = self.activeWindow.treeView for spot in self.structure.rootSpots(): treeView.expandSpot(spot) def selectRootSpot(self): """Select the first root spot in the tree. Does not signal an update. """ self.currentSelectionModel().selectSpots([self.structure. rootSpots()[0]], False) def currentSelectionModel(self): """Return the current tree's selection model. """ return self.activeWindow.treeView.selectionModel() def setActiveWin(self, window): """When a window is activated, stores it and emits a signal. Arguments: window -- the new active window """ self.activeWindow = window self.controlActivated.emit(self) self.updateCommandsAvail() def checkWindowClose(self, window): """Check for modified files and delete ref when a window is closing. Arguments: window -- the window being closed """ if len(self.windowList) > 1: self.windowList.remove(window) window.allowCloseFlag = True # # keep ref until Qt window can fully close # self.oldWindow = window elif self.checkSaveChanges(): window.allowCloseFlag = True self.controlClosed.emit(self) else: window.allowCloseFlag = False def checkSaveChanges(self): """Ask for save if doc modified, return True if OK to continue. Save this doc if directed. Return True if not modified, if saved or if discarded. Return False on cancel. """ if not self.modified or len(self.windowList) > 1: return True promptText = (_('Save changes to {}?').format(self.filePathObj) if self.filePathObj else _('Save changes?')) ans = QMessageBox.information(self.activeWindow, 'TreeLine', promptText, QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, QMessageBox.Save) if ans == QMessageBox.Save: self.fileSave() elif ans == QMessageBox.Cancel: return False else: self.deleteAutoSaveFile() return True def closeWindows(self): """Close this control's windows prior to quiting the application. """ for window in self.windowList: window.close() def autoSave(self): """Save a backup file if appropriate. Called from the timer. """ if self.filePathObj and not self.imported: self.fileSave(True) def resetAutoSave(self): """Start or stop the auto-save timer based on file modified status. Also delete old autosave files if file becomes unmodified. """ self.autoSaveTimer.stop() minutes = globalref.genOptions['AutoSaveMinutes'] if minutes and self.modified: self.autoSaveTimer.start(60000 * minutes) else: self.deleteAutoSaveFile() def deleteAutoSaveFile(self): """Delete an auto save file if it exists. """ filePath = pathlib.Path(str(self.filePathObj) + '~') if self.filePathObj and filePath.is_file(): try: filePath.unlink() except OSError: QMessageBox.warning(self.activeWindow, 'TreeLine', _('Error - could not delete backup file {}'). format(filePath)) def windowActions(self, startNum=1, active=False): """Return a list of window menu actions to select this file's windows. Arguments: startNum -- where to start numbering the action names active -- if True, activate the current active window """ actions = [] maxActionPathLength = 30 abbrevPath = str(self.filePathObj) if len(abbrevPath) > maxActionPathLength: truncLength = maxActionPathLength - 3 pos = abbrevPath.find(os.sep, len(abbrevPath) - truncLength) if pos < 0: pos = len(abbrevPath) - truncLength abbrevPath = '...' + abbrevPath[pos:] for window in self.windowList: action = QAction('&{0:d} {1}'.format(startNum, abbrevPath), self, statusTip=str(self.filePathObj), checkable=True) action.triggered.connect(window.activateAndRaise) if active and window == self.activeWindow: action.setChecked(True) actions.append(action) startNum += 1 return actions def setupActions(self): """Add the actions for contols at the local level. These actions affect an individual file, possibly in multiple windows. """ localActions = {} fileSaveAct = QAction(_('&Save'), self, toolTip=_('Save File'), statusTip=_('Save the current file')) fileSaveAct.setEnabled(False) fileSaveAct.triggered.connect(self.fileSave) localActions['FileSave'] = fileSaveAct fileSaveAsAct = QAction(_('Save &As...'), self, statusTip=_('Save the file with a new name')) fileSaveAsAct.triggered.connect(self.fileSaveAs) localActions['FileSaveAs'] = fileSaveAsAct fileExportAct = QAction(_('&Export...'), self, statusTip=_('Export the file in various other formats')) fileExportAct.triggered.connect(self.fileExport) localActions['FileExport'] = fileExportAct filePropertiesAct = QAction(_('Prop&erties...'), self, statusTip=_('Set file parameters like compression and encryption')) filePropertiesAct.triggered.connect(self.fileProperties) localActions['FileProperties'] = filePropertiesAct filePrintSetupAct = QAction(_('P&rint Setup...'), self, statusTip=_('Set margins, page size and other printing options')) filePrintSetupAct.triggered.connect(self.printData.printSetup) localActions['FilePrintSetup'] = filePrintSetupAct filePrintPreviewAct = QAction(_('Print Pre&view...'), self, statusTip=_('Show a preview of printing results')) filePrintPreviewAct.triggered.connect(self.printData.printPreview) localActions['FilePrintPreview'] = filePrintPreviewAct filePrintAct = QAction(_('&Print...'), self, statusTip=_('Print tree output based on current options')) filePrintAct.triggered.connect(self.printData.filePrint) localActions['FilePrint'] = filePrintAct filePrintPdfAct = QAction(_('Print &to PDF...'), self, statusTip=_('Export to PDF with current printing options')) filePrintPdfAct.triggered.connect(self.printData.filePrintPdf) localActions['FilePrintPdf'] = filePrintPdfAct editUndoAct = QAction(_('&Undo'), self, statusTip=_('Undo the previous action')) editUndoAct.triggered.connect(self.editUndo) localActions['EditUndo'] = editUndoAct editRedoAct = QAction(_('&Redo'), self, statusTip=_('Redo the previous undo')) editRedoAct.triggered.connect(self.editRedo) localActions['EditRedo'] = editRedoAct editCutAct = QAction(_('Cu&t'), self, statusTip=_('Cut the branch or text to the clipboard')) editCutAct.triggered.connect(self.editCut) localActions['EditCut'] = editCutAct editCopyAct = QAction(_('&Copy'), self, statusTip=_('Copy the branch or text to the clipboard')) editCopyAct.triggered.connect(self.editCopy) localActions['EditCopy'] = editCopyAct editPasteAct = QAction(_('&Paste'), self, statusTip=_('Paste nodes or text from the clipboard')) editPasteAct.triggered.connect(self.editPaste) localActions['EditPaste'] = editPasteAct editPastePlainAct = QAction(_('Pa&ste Plain Text'), self, statusTip=_('Paste non-formatted text from the clipboard')) editPastePlainAct.setEnabled(False) localActions['EditPastePlain'] = editPastePlainAct editPasteChildAct = QAction(_('Paste C&hild'), self, statusTip=_('Paste a child node from the clipboard')) editPasteChildAct.triggered.connect(self.editPasteChild) localActions['EditPasteChild'] = editPasteChildAct editPasteBeforeAct = QAction(_('Paste Sibling &Before'), self, statusTip=_('Paste a sibling before selection')) editPasteBeforeAct.triggered.connect(self.editPasteBefore) localActions['EditPasteBefore'] = editPasteBeforeAct editPasteAfterAct = QAction(_('Paste Sibling &After'), self, statusTip=_('Paste a sibling after selection')) editPasteAfterAct.triggered.connect(self.editPasteAfter) localActions['EditPasteAfter'] = editPasteAfterAct editPasteCloneChildAct = QAction(_('Paste Cl&oned Child'), self, statusTip=_('Paste a child clone from the clipboard')) editPasteCloneChildAct.triggered.connect(self.editPasteCloneChild) localActions['EditPasteCloneChild'] = editPasteCloneChildAct editPasteCloneBeforeAct = QAction(_('Paste Clo&ned Sibling Before'), self, statusTip=_('Paste a sibling clone before selection')) editPasteCloneBeforeAct.triggered.connect(self.editPasteCloneBefore) localActions['EditPasteCloneBefore'] = editPasteCloneBeforeAct editPasteCloneAfterAct = QAction(_('Paste Clone&d Sibling After'), self, statusTip=_('Paste a sibling clone after selection')) editPasteCloneAfterAct.triggered.connect(self.editPasteCloneAfter) localActions['EditPasteCloneAfter'] = editPasteCloneAfterAct nodeRenameAct = QAction(_('&Rename'), self, statusTip=_('Rename the current tree entry title')) nodeRenameAct.triggered.connect(self.nodeRename) localActions['NodeRename'] = nodeRenameAct nodeAddChildAct = QAction(_('Add &Child'), self, statusTip=_('Add new child to selected parent')) nodeAddChildAct.triggered.connect(self.nodeAddChild) localActions['NodeAddChild'] = nodeAddChildAct nodeInBeforeAct = QAction(_('Insert Sibling &Before'), self, statusTip=_('Insert new sibling before selection')) nodeInBeforeAct.triggered.connect(self.nodeInBefore) localActions['NodeInsertBefore'] = nodeInBeforeAct nodeInAfterAct = QAction(_('Insert Sibling &After'), self, statusTip=_('Insert new sibling after selection')) nodeInAfterAct.triggered.connect(self.nodeInAfter) localActions['NodeInsertAfter'] = nodeInAfterAct nodeDeleteAct = QAction(_('&Delete Node'), self, statusTip=_('Delete the selected nodes')) nodeDeleteAct.triggered.connect(self.nodeDelete) localActions['NodeDelete'] = nodeDeleteAct nodeIndentAct = QAction(_('&Indent Node'), self, statusTip=_('Indent the selected nodes')) nodeIndentAct.triggered.connect(self.nodeIndent) localActions['NodeIndent'] = nodeIndentAct nodeUnindentAct = QAction(_('&Unindent Node'), self, statusTip=_('Unindent the selected nodes')) nodeUnindentAct.triggered.connect(self.nodeUnindent) localActions['NodeUnindent'] = nodeUnindentAct nodeMoveUpAct = QAction(_('&Move Up'), self, statusTip=_('Move the selected nodes up')) nodeMoveUpAct.triggered.connect(self.nodeMoveUp) localActions['NodeMoveUp'] = nodeMoveUpAct nodeMoveDownAct = QAction(_('M&ove Down'), self, statusTip=_('Move the selected nodes down')) nodeMoveDownAct.triggered.connect(self.nodeMoveDown) localActions['NodeMoveDown'] = nodeMoveDownAct nodeMoveFirstAct = QAction(_('Move &First'), self, statusTip=_('Move the selected nodes to be the first children')) nodeMoveFirstAct.triggered.connect(self.nodeMoveFirst) localActions['NodeMoveFirst'] = nodeMoveFirstAct nodeMoveLastAct = QAction(_('Move &Last'), self, statusTip=_('Move the selected nodes to be the last children')) nodeMoveLastAct.triggered.connect(self.nodeMoveLast) localActions['NodeMoveLast'] = nodeMoveLastAct title = _('&Set Node Type') key = globalref.keyboardOptions['DataNodeType'] if not key.isEmpty(): title = '{0} ({1})'.format(title, key.toString()) self.typeSubMenu = QMenu(title, statusTip=_('Set the node type for selected nodes')) self.typeSubMenu.aboutToShow.connect(self.loadTypeSubMenu) self.typeSubMenu.triggered.connect(self.dataSetType) typeContextMenuAct = QAction(_('Set Node Type'), self.typeSubMenu) typeContextMenuAct.triggered.connect(self.showTypeContextMenu) localActions['DataNodeType'] = typeContextMenuAct dataCopyTypeAct = QAction(_('Copy Types from &File...'), self, statusTip=_('Copy the configuration from another TreeLine file')) dataCopyTypeAct.triggered.connect(self.dataCopyType) localActions['DataCopyType'] = dataCopyTypeAct dataRegenRefsAct = QAction(_('&Regenerate References'), self, statusTip=_('Force update of all conditional types & math fields')) dataRegenRefsAct.triggered.connect(self.dataRegenRefs) localActions['DataRegenRefs'] = dataRegenRefsAct dataCloneMatchesAct = QAction(_('Clone All &Matched Nodes'), self, statusTip=_('Convert all matching nodes into clones')) dataCloneMatchesAct.triggered.connect(self.dataCloneMatches) localActions['DataCloneMatches'] = dataCloneMatchesAct dataDetachClonesAct = QAction(_('&Detach Clones'), self, statusTip=_('Detach all cloned nodes in current branches')) dataDetachClonesAct.triggered.connect(self.dataDetachClones) localActions['DataDetachClones'] = dataDetachClonesAct dataFlatCatAct = QAction(_('Flatten &by Category'), self, statusTip=_('Collapse descendants by merging fields')) dataFlatCatAct.triggered.connect(self.dataFlatCategory) localActions['DataFlatCategory'] = dataFlatCatAct dataAddCatAct = QAction(_('Add Category &Level...'), self, statusTip=_('Insert category nodes above children')) dataAddCatAct.triggered.connect(self.dataAddCategory) localActions['DataAddCategory'] = dataAddCatAct dataSwapCatAct = QAction(_('S&wap Category Levels'), self, statusTip=_('Swap child and grandchild category nodes')) dataSwapCatAct.triggered.connect(self.dataSwapCategory) localActions['DataSwapCategory'] = dataSwapCatAct toolsSpellCheckAct = QAction(_('&Spell Check...'), self, statusTip=_('Spell check the tree\'s text data')) toolsSpellCheckAct.triggered.connect(self.toolsSpellCheck) localActions['ToolsSpellCheck'] = toolsSpellCheckAct formatBoldAct = QAction(_('&Bold Font'), self, statusTip=_('Set the current or selected font to bold'), checkable=True) formatBoldAct.setEnabled(False) localActions['FormatBoldFont'] = formatBoldAct formatItalicAct = QAction(_('&Italic Font'), self, statusTip=_('Set the current or selected font to italic'), checkable=True) formatItalicAct.setEnabled(False) localActions['FormatItalicFont'] = formatItalicAct formatUnderlineAct = QAction(_('U&nderline Font'), self, statusTip=_('Set the current or selected font to underline'), checkable=True) formatUnderlineAct.setEnabled(False) localActions['FormatUnderlineFont'] = formatUnderlineAct title = _('&Font Size') key = globalref.keyboardOptions['FormatFontSize'] if not key.isEmpty(): title = '{0} ({1})'.format(title, key.toString()) self.fontSizeSubMenu = QMenu(title, statusTip=_('Set size of the current or selected text')) sizeActions = QActionGroup(self) for size in (_('Small'), _('Default'), _('Large'), _('Larger'), _('Largest')): action = QAction(size, sizeActions) action.setCheckable(True) self.fontSizeSubMenu.addActions(sizeActions.actions()) self.fontSizeSubMenu.setEnabled(False) fontSizeContextMenuAct = QAction(_('Set Font Size'), self.fontSizeSubMenu) localActions['FormatFontSize'] = fontSizeContextMenuAct formatColorAct = QAction(_('Font C&olor...'), self, statusTip=_('Set the color of the current or selected text')) formatColorAct.setEnabled(False) localActions['FormatFontColor'] = formatColorAct formatExtLinkAct = QAction(_('&External Link...'), self, statusTip=_('Add or modify an extrnal web link')) formatExtLinkAct.setEnabled(False) localActions['FormatExtLink'] = formatExtLinkAct formatIntLinkAct = QAction(_('Internal &Link...'), self, statusTip=_('Add or modify an internal node link')) formatIntLinkAct.setEnabled(False) localActions['FormatIntLink'] = formatIntLinkAct formatInsDateAct = QAction(_('Insert &Date'), self, statusTip=_('Insert current date as text')) formatInsDateAct.setEnabled(False) localActions['FormatInsertDate'] = formatInsDateAct formatClearFormatAct = QAction(_('Clear For&matting'), self, statusTip=_('Clear current or selected text formatting')) formatClearFormatAct.setEnabled(False) localActions['FormatClearFormat'] = formatClearFormatAct winNewAct = QAction(_('&New Window'), self, statusTip=_('Open a new window for the same file')) winNewAct.triggered.connect(self.windowNew) localActions['WinNewWindow'] = winNewAct for name, action in localActions.items(): icon = globalref.toolIcons.getIcon(name.lower()) if icon: action.setIcon(icon) key = globalref.keyboardOptions[name] if not key.isEmpty(): action.setShortcut(key) typeIcon = globalref.toolIcons.getIcon('DataNodeType'.lower()) if typeIcon: self.typeSubMenu.setIcon(typeIcon) fontIcon = globalref.toolIcons.getIcon('FormatFontSize'.lower()) if fontIcon: self.fontSizeSubMenu.setIcon(fontIcon) self.allActions.update(localActions) def fileSave(self, backupFile=False): """Save the currently active file. Arguments: backupFile -- if True, write auto-save backup file instead """ if not self.filePathObj or self.imported: self.fileSaveAs() return QApplication.setOverrideCursor(Qt.WaitCursor) savePathObj = self.filePathObj if backupFile: savePathObj = pathlib.Path(str(savePathObj) + '~') else: self.structure.purgeOldFieldData() fileData = self.structure.fileData() fileData['properties'].update(self.printData.fileData()) if self.spellCheckLang: fileData['properties']['spellchk'] = self.spellCheckLang if not self.compressed and not self.encrypted: indent = 3 if globalref.genOptions['PrettyPrint'] else 0 try: with savePathObj.open('w', encoding='utf-8', newline='\n') as f: json.dump(fileData, f, indent=indent, sort_keys=True) except IOError: QApplication.restoreOverrideCursor() QMessageBox.warning(self.activeWindow, 'TreeLine', _('Error - could not write to {}'). format(savePathObj)) return else: data = json.dumps(fileData, indent=0, sort_keys=True).encode() if self.compressed: data = gzip.compress(data) if self.encrypted: password = (globalref.mainControl.passwords. get(self.filePathObj, '')) if not password: QApplication.restoreOverrideCursor() dialog = miscdialogs.PasswordDialog(True, '', self.activeWindow) if dialog.exec_() != QDialog.Accepted: return QApplication.setOverrideCursor(Qt.WaitCursor) password = dialog.password if miscdialogs.PasswordDialog.remember: globalref.mainControl.passwords[self. filePathObj] = password data = (treemaincontrol.encryptPrefix + p3.p3_encrypt(data, password.encode())) try: with savePathObj.open('wb') as f: f.write(data) except IOError: QApplication.restoreOverrideCursor() QMessageBox.warning(self.activeWindow, 'TreeLine', _('Error - could not write to {}'). format(savePathObj)) return QApplication.restoreOverrideCursor() if not backupFile: fileInfoFormat = self.structure.treeFormats.fileInfoFormat fileInfoFormat.updateFileInfo(self.filePathObj, self.structure.fileInfoNode) self.setModified(False) self.imported = False self.activeWindow.statusBar().showMessage(_('File saved'), 3000) def fileSaveAs(self): """Prompt for a new file name and save the file. """ oldPathObj = self.filePathObj oldModifiedFlag = self.modified oldImportFlag = self.imported self.modified = True self.imported = False filters = ';;'.join((globalref.fileFilters['trlnsave'], globalref.fileFilters['trlngz'], globalref.fileFilters['trlnenc'])) initFilter = globalref.fileFilters['trlnsave'] defaultPathObj = globalref.mainControl.defaultPathObj() if defaultPathObj.is_file(): defaultPathObj = defaultPathObj.with_suffix('.trln') newPath, selectFilter = (QFileDialog. getSaveFileName(self.activeWindow, _('TreeLine - Save As'), str(defaultPathObj), filters, initFilter)) if newPath: self.filePathObj = pathlib.Path(newPath) if not self.filePathObj.suffix: self.filePathObj = self.filePathObj.with_suffix('.trln') if selectFilter != initFilter: self.compressed = (selectFilter == globalref.fileFilters['trlngz']) self.encrypted = (selectFilter == globalref.fileFilters['trlnenc']) self.fileSave() if not self.modified: globalref.mainControl.recentFiles.addItem(self.filePathObj) self.updateWindowCaptions() return self.filePathObj = oldPathObj self.modified = oldModifiedFlag self.imported = oldImportFlag def fileExport(self): """Export the file in various other formats. """ exportControl = exports.ExportControl(self.structure, self.currentSelectionModel(), globalref.mainControl. defaultPathObj(), self.printData) try: exportControl.interactiveExport() except IOError: QApplication.restoreOverrideCursor() QMessageBox.warning(self.activeWindow, 'TreeLine', _('Error - could not write to file')) def fileProperties(self): """Show dialog to set file parameters like compression and encryption. """ origZeroBlanks = self.structure.mathZeroBlanks dialog = miscdialogs.FilePropertiesDialog(self, self.activeWindow) if dialog.exec_() == QDialog.Accepted: self.setModified() if self.structure.mathZeroBlanks != origZeroBlanks: self.updateAll(False) def editUndo(self): """Undo the previous action and update the views. """ self.structure.undoList.undo() self.updateAll(False) def editRedo(self): """Redo the previous undo and update the views. """ self.structure.redoList.undo() self.updateAll(False) def editCut(self): """Cut the branch or text to the clipboard. """ widget = QApplication.focusWidget() try: if widget.hasSelectedText(): widget.cut() return except AttributeError: pass self.currentSelectionModel().copySelectedNodes() selSpots = self.currentSelectionModel().selectedSpots() rootSpots = [spot for spot in selSpots if not spot.parentSpot.parentSpot] if selSpots and len(rootSpots) < len(self.structure.childList): self.nodeDelete() def editCopy(self): """Copy the branch or text to the clipboard. Copy from any selection in non-focused output view, or copy from any focused editor, or copy from tree. """ widgets = [QApplication.focusWidget()] splitter = self.activeWindow.rightTabs.currentWidget() if splitter == self.activeWindow.outputSplitter: widgets[0:0] = [splitter.widget(0), splitter.widget(1)] for widget in widgets: try: if widget.hasSelectedText(): widget.copy() return except AttributeError: pass self.currentSelectionModel().copySelectedNodes() def editPaste(self): """Paste nodes or text from the clipboard. """ if self.activeWindow.treeView.hasFocus(): self.editPasteChild() else: widget = QApplication.focusWidget() try: widget.paste() except AttributeError: pass def editPasteChild(self): """Paste a child node from the clipboard. """ if (self.currentSelectionModel().selectedSpots(). pasteChild(self.structure, self.activeWindow.treeView)): self.updateAll() globalref.mainControl.updateConfigDialog() def editPasteBefore(self): """Paste a sibling before selection. """ treeView = self.activeWindow.treeView selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([spot.parentSpot.childSpots() for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) if selSpots.pasteSibling(self.structure): treeView.restoreExpandState(expandState) self.currentSelectionModel().selectSpots(selSpots, False) self.updateAll() globalref.mainControl.updateConfigDialog() def editPasteAfter(self): """Paste a sibling after selection. """ treeView = self.activeWindow.treeView selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([spot.parentSpot.childSpots() for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) if selSpots.pasteSibling(self.structure, False): treeView.restoreExpandState(expandState) self.currentSelectionModel().selectSpots(selSpots, False) self.updateAll() globalref.mainControl.updateConfigDialog() def editPasteCloneChild(self): """Paste a child clone from the clipboard. """ if (self.currentSelectionModel().selectedSpots(). pasteCloneChild(self.structure, self.activeWindow.treeView)): self.updateAll() def editPasteCloneBefore(self): """Paste a sibling clone before selection. """ selSpots = self.currentSelectionModel().selectedSpots() if selSpots.pasteCloneSibling(self.structure): self.currentSelectionModel().selectSpots(selSpots, False) self.updateAll() def editPasteCloneAfter(self): """Paste a sibling clone after selection. """ selSpots = self.currentSelectionModel().selectedSpots() if selSpots.pasteCloneSibling(self.structure, False): self.currentSelectionModel().selectSpots(selSpots, False) self.updateAll() def nodeRename(self): """Start the rename editor in the selected tree node. """ if self.activeWindow.treeFilterView: self.activeWindow.treeFilterView.editItem(self.activeWindow. treeFilterView. currentItem()) else: self.activeWindow.treeView.endEditing() self.activeWindow.treeView.edit(self.currentSelectionModel(). currentIndex()) def nodeAddChild(self): """Add new child to selected parent. """ self.activeWindow.treeView.endEditing() selSpots = self.currentSelectionModel().selectedSpots() newSpots = selSpots.addChild(self.structure, self.activeWindow.treeView) self.updateAll() if globalref.genOptions['RenameNewNodes']: self.currentSelectionModel().selectSpots(newSpots) if len(newSpots) == 1: self.activeWindow.treeView.edit(newSpots[0].index(self.model)) def nodeInBefore(self): """Insert new sibling before selection. """ treeView = self.activeWindow.treeView treeView.endEditing() selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([spot.parentSpot.childSpots() for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) newSpots = selSpots.insertSibling(self.structure) treeView.restoreExpandState(expandState) self.updateAll() if globalref.genOptions['RenameNewNodes']: self.currentSelectionModel().selectSpots(newSpots) if len(newSpots) == 1: treeView.edit(newSpots[0].index(self.model)) def nodeInAfter(self): """Insert new sibling after selection. """ treeView = self.activeWindow.treeView treeView.endEditing() selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([spot.parentSpot.childSpots() for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) newSpots = selSpots.insertSibling(self.structure, False) treeView.restoreExpandState(expandState) self.updateAll() if globalref.genOptions['RenameNewNodes']: self.currentSelectionModel().selectSpots(newSpots) if len(newSpots) == 1: treeView.edit(newSpots[0].index(self.model)) def nodeDelete(self): """Delete the selected nodes. """ treeView = self.activeWindow.treeView selSpots = self.currentSelectionModel().selectedBranchSpots() if selSpots: # collapse deleted items to avoid crash for spot in selSpots: treeView.collapseSpot(spot) # clear hover to avoid crash if deleted child item was hovered over self.activeWindow.treeView.clearHover() # clear selection to avoid invalid multiple selection bug self.currentSelectionModel().selectSpots([], False) # clear selections in other windows that are about to be deleted for window in self.windowList: if window != self.activeWindow: selectModel = window.treeView.selectionModel() ancestors = set() for spot in selectModel.selectedBranchSpots(): ancestors.update(set(spot.spotChain())) if ancestors & set(selSpots): selectModel.selectSpots([], False) saveSpots = chain.from_iterable([spot.parentSpot.childSpots() for spot in selSpots]) saveSpots = set(saveSpots) - set(selSpots) expandState = treeView.savedExpandState(saveSpots) nextSel = selSpots.delete(self.structure) treeView.restoreExpandState(expandState) self.currentSelectionModel().selectSpots([nextSel]) self.updateAll() def nodeIndent(self): """Indent the selected nodes. Makes them children of their previous siblings. """ treeView = self.activeWindow.treeView selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([spot.parentSpot.childSpots() for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) newSpots = selSpots.indent(self.structure) treeView.restoreExpandState(expandState) for spot in selSpots: treeView.expandSpot(spot.parentSpot) self.currentSelectionModel().selectSpots(newSpots, False) self.updateAll() def nodeUnindent(self): """Unindent the selected nodes. Makes them their parent's next sibling. """ treeView = self.activeWindow.treeView selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([spot.parentSpot.childSpots() for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) newSpots = selSpots.unindent(self.structure) treeView.restoreExpandState(expandState) self.currentSelectionModel().selectSpots(newSpots, False) self.updateAll() def nodeMoveUp(self): """Move the selected nodes upward in the sibling list. """ treeView = self.activeWindow.treeView selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([(spot, spot.prevSiblingSpot()) for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) selSpots.move(self.structure) self.updateAll() treeView.restoreExpandState(expandState) self.currentSelectionModel().selectSpots(selSpots) def nodeMoveDown(self): """Move the selected nodes downward in the sibling list. """ treeView = self.activeWindow.treeView selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([(spot, spot.nextSiblingSpot()) for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) selSpots.move(self.structure, False) self.updateAll() treeView.restoreExpandState(expandState) self.currentSelectionModel().selectSpots(selSpots) def nodeMoveFirst(self): """Move the selected nodes to be the first children. """ treeView = self.activeWindow.treeView selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([(spot, spot.parentSpot.childSpots()[0]) for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) selSpots.moveToEnd(self.structure) self.updateAll() treeView.restoreExpandState(expandState) self.currentSelectionModel().selectSpots(selSpots) def nodeMoveLast(self): """Move the selected nodes to be the last children. """ treeView = self.activeWindow.treeView selSpots = self.currentSelectionModel().selectedSpots() saveSpots = chain.from_iterable([(spot, spot.parentSpot.childSpots()[-1]) for spot in selSpots]) expandState = treeView.savedExpandState(saveSpots) selSpots.moveToEnd(self.structure, False) self.updateAll() treeView.restoreExpandState(expandState) self.currentSelectionModel().selectSpots(selSpots) def dataSetType(self, action): """Change the type of selected nodes based on a menu selection. Arguments: action -- the menu action containing the new type name """ newType = action.toolTip() # gives menu name without the accelerator nodes = [node for node in self.currentSelectionModel().selectedNodes() if node.formatRef.name != newType] if nodes: undo.TypeUndo(self.structure.undoList, nodes) for node in nodes: node.changeDataType(self.structure.treeFormats[newType]) self.updateAll() def loadTypeSubMenu(self): """Update type select submenu with type names and check marks. """ selectTypeNames = set() typeLimitNames = set() for node in self.currentSelectionModel().selectedNodes(): selectTypeNames.add(node.formatRef.name) if typeLimitNames is not None: for parent in node.parents(): limit = (parent.formatRef.childTypeLimit if parent.formatRef else None) if (not limit or (typeLimitNames and limit != typeLimitNames)): typeLimitNames = None elif typeLimitNames is not None: typeLimitNames = limit if typeLimitNames: typeNames = sorted(list(typeLimitNames)) else: typeNames = self.structure.treeFormats.typeNames() self.typeSubMenu.clear() usedShortcuts = [] for name in typeNames: shortcutPos = 0 try: while [shortcutPos] in usedShortcuts: shortcutPos += 1 usedShortcuts.append(name[shortcutPos]) text = '{0}&{1}'.format(name[:shortcutPos], name[shortcutPos:]) except IndexError: text = name action = self.typeSubMenu.addAction(text) action.setCheckable(True) if name in selectTypeNames: action.setChecked(True) def showTypeContextMenu(self): """Show a type set menu at the current tree view item. """ self.activeWindow.treeView.showTypeMenu(self.typeSubMenu) def dataCopyType(self): """Copy the configuration from another TreeLine file. """ filters = ';;'.join((globalref.fileFilters['trlnv3'], globalref.fileFilters['all'])) fileName, selectFilter = QFileDialog.getOpenFileName(self.activeWindow, _('TreeLine - Open Configuration File'), str(globalref.mainControl. defaultPathObj(True)), filters) if not fileName: return QApplication.setOverrideCursor(Qt.WaitCursor) newStructure = None try: with open(fileName, 'r', encoding='utf-8') as f: fileData = json.load(f) newStructure = treestructure.TreeStructure(fileData, addSpots=False) except IOError: pass except (ValueError, KeyError, TypeError): fileObj = open(fileName, 'rb') fileObj, encrypted = globalref.mainControl.decryptFile(fileObj) if not fileObj: QApplication.restoreOverrideCursor() return fileObj, compressed = globalref.mainControl.decompressFile(fileObj) if compressed or encrypted: try: textFileObj = io.TextIOWrapper(fileObj, encoding='utf-8') fileData = json.load(textFileObj) textFileObj.close() newStructure = treestructure.TreeStructure(fileData, addSpots=False) except (ValueError, KeyError, TypeError): pass fileObj.close() if not newStructure: QApplication.restoreOverrideCursor() QMessageBox.warning(self.activeWindow, 'TreeLine', _('Error - could not read file {0}'). format(fileName)) return undo.FormatUndo(self.structure.undoList, self.structure.treeFormats, treeformats.TreeFormats()) for nodeFormat in newStructure.treeFormats.values(): self.structure.treeFormats.addTypeIfMissing(nodeFormat) QApplication.restoreOverrideCursor() self.updateAll() globalref.mainControl.updateConfigDialog() def dataRegenRefs(self): """Force update of all conditional types & math fields. """ self.updateAll(False) def dataCloneMatches(self): """Convert all matching nodes into clones. """ QApplication.setOverrideCursor(Qt.WaitCursor) selSpots = self.currentSelectionModel().selectedSpots() titleDict = {} for node in self.structure.nodeDict.values(): titleDict.setdefault(node.title(), set()).add(node) undoObj = undo.ChildListUndo(self.structure.undoList, self.structure.childList, addBranch=True) numChanges = 0 for node in self.structure.descendantGen(): matches = titleDict[node.title()] if len(matches) > 1: matches = matches.copy() matches.remove(node) for matchedNode in matches: if node.isIdentical(matchedNode): numChanges += 1 if len(matchedNode.spotRefs) > len(node.spotRefs): tmpNode = node node = matchedNode matchedNode = tmpNode numSpots = len(matchedNode.spotRefs) for parent in matchedNode.parents(): pos = parent.childList.index(matchedNode) parent.childList[pos] = node node.addSpotRef(parent) for child in matchedNode.descendantGen(): if len(child.spotRefs) <= numSpots: titleDict[child.title()].remove(child) self.structure.removeNodeDictRef(child) child.removeInvalidSpotRefs(False) if numChanges: msg = _('Converted {0} branches into clones').format(numChanges) self.currentSelectionModel().selectSpots([spot for spot in selSpots if spot.isValid()], False) self.updateAll() else: msg = _('No identical nodes found') self.structure.undoList.removeLastUndo(undoObj) QApplication.restoreOverrideCursor() QMessageBox.information(self.activeWindow, 'TreeLine', msg) def dataDetachClones(self): """Detach all cloned nodes in current branches. """ QApplication.setOverrideCursor(Qt.WaitCursor) selSpots = self.currentSelectionModel().selectedBranchSpots() undoObj = undo.ChildListUndo(self.structure.undoList, [spot.parentSpot.nodeRef for spot in selSpots], addBranch=True) numChanges = 0 for branchSpot in selSpots: for spot in branchSpot.spotDescendantGen(): if (len(spot.nodeRef.spotRefs) > 1 and len(spot.parentSpot.nodeRef.spotRefs) <= 1): numChanges += 1 linkedNode = spot.nodeRef linkedNode.spotRefs.remove(spot) newNode = treenode.TreeNode(linkedNode.formatRef) newNode.data = linkedNode.data.copy() newNode.childList = linkedNode.childList[:] newNode.spotRefs.add(spot) spot.nodeRef = newNode parent = spot.parentSpot.nodeRef pos = parent.childList.index(linkedNode) parent.childList[pos] = newNode self.structure.addNodeDictRef(newNode) if numChanges: self.updateAll() else: self.structure.undoList.removeLastUndo(undoObj) QApplication.restoreOverrideCursor() def dataFlatCategory(self): """Collapse descendant nodes by merging fields. Overwrites data in any fields with the same name. """ QApplication.setOverrideCursor(Qt.WaitCursor) selectList = self.currentSelectionModel().selectedBranches() undo.ChildDataUndo(self.structure.undoList, selectList, True, self.structure.treeFormats) origFormats = self.structure.undoList[-1].treeFormats for node in selectList: node.flatChildCategory(origFormats, self.structure) self.updateAll() globalref.mainControl.updateConfigDialog() QApplication.restoreOverrideCursor() def dataAddCategory(self): """Insert category nodes above children. """ selectList = self.currentSelectionModel().selectedBranches() children = [] for node in selectList: children.extend(node.childList) fieldList = self.structure.treeFormats.commonFields(children) if not fieldList: QMessageBox.warning(self.activeWindow, 'TreeLine', _('Cannot expand without common fields')) return dialog = miscdialogs.FieldSelectDialog(_('Category Fields'), _('Select fields for new level'), fieldList, self.activeWindow) if dialog.exec_() != QDialog.Accepted: return QApplication.setOverrideCursor(Qt.WaitCursor) undo.ChildDataUndo(self.structure.undoList, selectList, True, self.structure.treeFormats) for node in selectList: node.addChildCategory(dialog.selectedFields, self.structure) self.updateAll() globalref.mainControl.updateConfigDialog() QApplication.restoreOverrideCursor() def dataSwapCategory(self): """Swap child and grandchild category nodes. """ QApplication.setOverrideCursor(Qt.WaitCursor) selectList = self.currentSelectionModel().selectedBranches() undo.ChildListUndo(self.structure.undoList, selectList, addBranch=True) doneNodes = set() for ancestor in selectList: for child in ancestor.childList[:]: for catNode in child.childList[:]: if catNode not in doneNodes: doneNodes.add(catNode) childSpots = [spot.parentSpot for spot in catNode.spotRefs] childSpots.sort(key=operator.methodcaller('sortKey')) children = [childSpot.nodeRef for childSpot in childSpots] catNode.childList[0:0] = children for ancestor in selectList: position = 0 doneNodes = set() for child in ancestor.childList[:]: for catNode in child.childList[:]: if catNode not in doneNodes: doneNodes.add(catNode) for catSpot in catNode.spotRefs: child = catSpot.parentSpot.nodeRef child.childList = [] if child in ancestor.childList: position = ancestor.childList.index(child) del ancestor.childList[position] ancestor.childList.insert(position, catNode) position += 1 catNode.addSpotRef(ancestor) catNode.removeInvalidSpotRefs() self.updateAll() QApplication.restoreOverrideCursor() def toolsSpellCheck(self): """Spell check the tree text data. """ try: spellCheckOp = spellcheck.SpellCheckOperation(self) except spellcheck.SpellCheckError: return spellCheckOp.spellCheck() def findNodesByWords(self, wordList, titlesOnly=False, forward=True): """Search for and select nodes that match the word list criteria. Called from the text find dialog. Returns True if found, otherwise False. Arguments: wordList -- a list of words or phrases to find titleOnly -- search only in the title text if True forward -- next if True, previous if False """ currentSpot = self.currentSelectionModel().currentSpot() spot = currentSpot while True: if self.activeWindow.treeFilterView: spot = self.activeWindow.treeFilterView.nextPrevSpot(spot, forward) else: if forward: spot = spot.nextTreeSpot(True) else: spot = spot.prevTreeSpot(True) if spot is currentSpot: return False if spot.nodeRef.wordSearch(wordList, titlesOnly, spot): self.currentSelectionModel().selectSpots([spot], True, True) rightView = self.activeWindow.rightParentView() if not rightView: # view update required if (and only if) view is newly shown QApplication.processEvents() rightView = self.activeWindow.rightParentView() if rightView: rightView.highlightSearch(wordList=wordList) QApplication.processEvents() return True def findNodesByRegExp(self, regExpList, titlesOnly=False, forward=True): """Search for and select nodes that match the regular exp criteria. Called from the text find dialog. Returns True if found, otherwise False. Arguments: regExpList -- a list of regular expression objects titleOnly -- search only in the title text if True forward -- next if True, previous if False """ currentSpot = self.currentSelectionModel().currentSpot() spot = currentSpot while True: if self.activeWindow.treeFilterView: spot = self.activeWindow.treeFilterView.nextPrevSpot(spot, forward) else: if forward: spot = spot.nextTreeSpot(True) else: spot = spot.prevTreeSpot(True) if spot is currentSpot: return False if spot.nodeRef.regExpSearch(regExpList, titlesOnly, spot): self.currentSelectionModel().selectSpots([spot], True, True) rightView = self.activeWindow.rightParentView() if not rightView: # view update required if (and only if) view is newly shown QApplication.processEvents() rightView = self.activeWindow.rightParentView() if rightView: rightView.highlightSearch(regExpList=regExpList) return True def findNodesByCondition(self, conditional, forward=True): """Search for and select nodes that match the regular exp criteria. Called from the conditional find dialog. Returns True if found, otherwise False. Arguments: conditional -- the Conditional object to be evaluated forward -- next if True, previous if False """ currentSpot = self.currentSelectionModel().currentSpot() spot = currentSpot while True: if self.activeWindow.treeFilterView: spot = self.activeWindow.treeFilterView.nextPrevSpot(spot, forward) else: if forward: spot = spot.nextTreeSpot(True) else: spot = spot.prevTreeSpot(True) if spot is currentSpot: return False if conditional.evaluate(spot.nodeRef): self.currentSelectionModel().selectSpots([spot], True, True) return True def findNodesForReplace(self, searchText='', regExpObj=None, typeName='', fieldName='', forward=True): """Search for & select nodes that match the criteria prior to replace. Called from the find replace dialog. Returns True if found, otherwise False. Arguments: searchText -- the text to find if no regexp is given regExpObj -- the regular expression to find if given typeName -- if given, verify that this node matches this type fieldName -- if given, only find matches under this type name forward -- next if True, previous if False """ currentSpot = self.currentSelectionModel().currentSpot() lastFoundSpot, currentNumMatches = self.findReplaceSpotRef numMatches = currentNumMatches if lastFoundSpot is not currentSpot: numMatches = 0 spot = currentSpot if not forward: if numMatches == 0: numMatches = -1 # find last one if backward elif numMatches == 1: numMatches = sys.maxsize # no match if on first one else: numMatches -= 2 while True: matchedField, numMatches, fieldPos = (spot.nodeRef. searchReplace(searchText, regExpObj, numMatches, typeName, fieldName)) if matchedField: fieldNum = (spot.nodeRef.formatRef.fieldNames(). index(matchedField)) self.currentSelectionModel().selectSpots([spot], True, True) self.activeWindow.rightTabs.setCurrentWidget(self.activeWindow. editorSplitter) dataView = self.activeWindow.rightParentView() if not dataView: # view update required if (and only if) view is newly shown QApplication.processEvents() dataView = self.activeWindow.rightParentView() if dataView: dataView.highlightMatch(searchText, regExpObj, fieldNum, fieldPos - 1) self.findReplaceSpotRef = (spot, numMatches) return True if self.activeWindow.treeFilterView: node = self.activeWindow.treeFilterView.nextPrevSpot(spot, forward) else: if forward: spot = spot.nextTreeSpot(True) else: spot = spot.prevTreeSpot(True) if spot is currentSpot and currentNumMatches == 0: self.findReplaceSpotRef = (None, 0) return False numMatches = 0 if forward else -1 def replaceInCurrentNode(self, searchText='', regExpObj=None, typeName='', fieldName='', replaceText=None): """Replace the current match in the current node. Called from the find replace dialog. Returns True if replaced, otherwise False. Arguments: searchText -- the text to find if no regexp is given regExpObj -- the regular expression to find if given typeName -- if given, verify that this node matches this type fieldName -- if given, only find matches under this type name replaceText -- if not None, replace a match with this string """ spot = self.currentSelectionModel().currentSpot() lastFoundSpot, numMatches = self.findReplaceSpotRef if numMatches > 0: numMatches -= 1 if lastFoundSpot is not spot: numMatches = 0 dataUndo = undo.DataUndo(self.structure.undoList, spot.nodeRef) matchedField, num1, num2 = (spot.nodeRef. searchReplace(searchText, regExpObj, numMatches, typeName, fieldName, replaceText)) if ((searchText and searchText in replaceText) or (regExpObj and r'\g<0>' in replaceText) or (regExpObj and regExpObj.pattern.startswith('(') and regExpObj.pattern.endswith(')') and r'\1' in replaceText)): numMatches += 1 # check for recursive matches self.findReplaceSpotRef = (spot, numMatches) if matchedField: self.updateTreeNode(spot.nodeRef) self.updateRightViews() return True self.structure.undoList.removeLastUndo(dataUndo) return False def replaceAll(self, searchText='', regExpObj=None, typeName='', fieldName='', replaceText=None): """Replace all matches in all nodes. Called from the find replace dialog. Returns number of matches replaced. Arguments: searchText -- the text to find if no regexp is given regExpObj -- the regular expression to find if given typeName -- if given, verify that this node matches this type fieldName -- if given, only find matches under this type name replaceText -- if not None, replace a match with this string """ QApplication.setOverrideCursor(Qt.WaitCursor) dataUndo = undo.DataUndo(self.structure.undoList, self.structure.childList, addBranch=True) totalMatches = 0 for node in self.structure.nodeDict.values(): field, matchQty, num = node.searchReplace(searchText, regExpObj, 0, typeName, fieldName, replaceText, True) totalMatches += matchQty self.findReplaceSpotRef = (None, 0) if totalMatches > 0: self.updateAll(True) else: self.structure.undoList.removeLastUndo(dataUndo) QApplication.restoreOverrideCursor() return totalMatches def windowNew(self, checked=False, offset=30): """Open a new window for this file. Arguments: checked -- unused parameter needed by QAction signal offset -- location offset from previously saved position """ window = treewindow.TreeWindow(self.model, self.allActions) self.setWindowSignals(window) window.winMinimized.connect(globalref.mainControl.trayMinimize) self.windowList.append(window) self.updateWindowCaptions() oldControl = globalref.mainControl.activeControl if oldControl: try: oldControl.activeWindow.saveWindowGeom() except RuntimeError: # possibly avoid rare error of deleted c++ TreeWindow pass window.restoreWindowGeom(offset) self.activeWindow = window self.expandRootNodes() self.selectRootSpot() window.show() window.updateRightViews() TreeLine/icons/0000755000175000017500000000000013262465526012371 5ustar dougdougTreeLine/icons/toolbar/0000755000175000017500000000000013262465526014033 5ustar dougdougTreeLine/icons/toolbar/32x32/0000755000175000017500000000000013635455551014615 5ustar dougdougTreeLine/icons/toolbar/32x32/viewnextselect.png0000644000175000017500000000315713262465526020401 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ٶQUtuHy@ف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/printpreviewzoomall.png0000644000175000017500000000356513262465526021467 0ustar dougdougPNG  IHDR szzsRGBbKGD ^ pHYs  tIME  ("NIDATXŗklޙٷm^;vH,U(XW R!Tm*EZJFDZySZ(ؐ.ڬػ>gtcWι{. !lZ/lP ` xNo9҆LǏ?t斖-UUՕǝH$>{w;Q_ݍ*"bvݻSN޾]"| Vb?S|#GN9s!|(2czIs_ڣw^YM8/^l[@~O;wD P<7(v7;+}.]d|i5≞(Epk8]]]߯ʔr)+6{\B:{;<MDl`:;;Ҷi^baGp{`Xn@1Yxvvvd]|ַ5vW\$ߚiTU␫!`(s){W f*e5_ik39 sbRW&q8{{/t߂B>=8? kԘvZ@/r3ZJ"8ud9v M}S9,$RHe 9ٿCs\( 朻1Ub8#'8,K*c sihT,Sv;K$ݭ #f84C B ʥ5RKeY+b*Fht:qnigR,ŲR* Mסظ5L"2%"DEK k̆h&[mm-%1O! 1 &Ţ_'_Tj pFtɉuσI=k@ fs)ФeymZ?Qw+MInQY l}xrlK6Jє>9X>o0^݈pc씹-3T8{IK/W]r%yi`y#99Wx=]Six7KsvҟCoooRUU^ 8k!DHzkmw\@kk+N'$IiH&9W&SSS\.[/WHKւXaOD`{ \.W(e$95MKEUUgG"|>kGm:MڼNG!OMς'zw,LPb<d;ll|gI#F`x#*h-].Hݬk^VJgN[!{.BjQjRjDIQL0fnd>v~Q"p?tiXADk&ND)$ yfZ.G'ȄХu@16sDn- ]~u =`_j'@a "SNs!ZD!Y"]zm twݝ3bb`L / i|xAȀ_d3b6< @7ިo7pG' T˚^Ǟ5²p/|Z~qww,By0af2,s=lL% g@rdxBDIENDB`TreeLine/icons/toolbar/32x32/editpaste.png0000644000175000017500000000174513262465526017313 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  IDATXMlTUЙtNLL-0FV]Uх,Q!DV$k4.LЅ .\W##&c06P7)o:m7?9{sd2iVw|i𚘚ȞUІq^ԁE!挄|(#&/G$(u~,b~ |1Hb(?͊cx e&鹴vۉ^='Ji 3SN~3IhW*V/LN@& =۞}sߩ>}p8{<%3WH $ɩ9n5 feXکxD)fnw?77"5 9(ݡ7iQPq0+xw&4Oưc K"ŷ_8=W8?wAyoFΤFYwsNΝF$5/[~N~p5ϩ EEGW(#;N-vp OѶG4NW(}}z%һup ւX[-Q\H;JݺW2giޤ_AX4=v^yv Lf0Uۮ[֞N$IUW@#lpT#)UgvuFTyjҪ<  = $b/[3`Th5. :`7`@ +P!kuV:u; e30w`=Gr{+v0ʵՊE ѲE\:QX.-9Lǿ7+?˗>:٧ v ŧ7r g/:obgOԧJQIENDB`TreeLine/icons/toolbar/32x32/datavisualconfig.png0000644000175000017500000001164213433405374020644 0ustar dougdougPNG  IHDR szz zTXtRaw profile type exifxڭir$ >Bqp ||nIh&lo$vm$Dq'dOjΠ>_)t}yJz?weSzx`̚}]}?(q~_,|\T2Ž>>֍!bw%Hboع/ǟC qoF:Gx9_)ޛqQޖr?Oe/|}|w,1waxUŒ/ ,+LPkwv盾2\<}w|>~Ɗue9*So h7̕ g1q+^#ٓ/Bb|'h>P'|:+owlb̀S0s,@CVJJp[,[jz9e9,%TRJ-kVs-V{ -"ar+ZLyt0HFeFgi32l"W^ŭoӶwu\;c'riFg/5FM{]Bå|Cex&\T l,{@ڇ)ceN9H.jN6yBA!g$Z)ż arӑ!mtc;3CvLr(e(m ghu:Y[#yuGV<$yW-rrWXC A|XT| bpn`œ􂦓ollps$Xovp0]\T!pc}B3XV1,UCr5 =TvOtr EZGi"O#=":ORBqפYG]efe;cO3E13D+΄"6:tm}lܠ+[817$Q1.YZQ`=E`,)q}/*A>W< ,.GҢ>C0;u&T$2|>RrzD6'OdeƎFd'aB]2 ׮6H|]ϠƊ%t{}B_&gBQ$2^| D;drc&=]HzղF+[r[*,5*iLnVȇoGo٠dLǠ:% )w`fB ^V<#Bk&uaV]K)pG,<(p ҝxTC~TJ<8qQS5#LԌBwTZ`.rFΒ6#aVL "?X+YI_TG69+2Z&/Y) L)h#N]2+MΎ{,e-Uzݔգ$DՕ0D,;qSV܃2Gկy*\j4O^>Z^sS x CFa&+GS!(U4!DR|ԋ~O"OLLdv >BeoZ+Pj;!&l`s$Q. 5t'wgۖPb"؏xj/Rj;drK`+%" aStp" m^U @H2R{#!ySx|30548SNw-y3+yxSrH|!KJ1;ސ]=,dpN5HsҮCϕsi*U%@u# à~I"WXu.o<3jr)Oޓ}CC7gn)/El~}#p͠ٯIb¹/-KLOvT?w@wkɖ2͐k3C $i¬0RēP?ul=1ACml/7u5@Kt(< OU ~74wi)nw{ ^Xr**w(UtG%5i=nhmM$tUÆ^b=q~'9t ?\vQ:۹ﲰe/߹ujWfobKGD pHYs  tIME,:/<#eIDATXŗMl\WM졸 mLXEU Q6 +*(BHBۈ EpJCh͸LB$m96q=3{c Gyw{6###{Nf2ƘʁR jN6=?11B0)" "[XXΝ;'N````n!i6:o>ZZZ=:Mc\(QY30>ksvrcxF%ҭ9~$m{11FMh^ƯT{Cuz &HD5$g.n9(f~ }\R6V-}DHzθ=(>ϝ~y(Χaj lU $kl: X$΂o's?yߙQpl/䪚juqE"RpS{G_`s ρS#r Dfךr ,~92߼ӏ Qqܰݟ M;8Za~ljB׾|e~?^n9wJL[MWF][JOш8׾Y&+D"=QI#ĥ'eFFH)á6ͽYp'/IgzȑHoS>)-tc.; @sJnnPN8s4Fd(<' X|ZQF>_ Z2uXA\D'nvz P{j6al]'"8R 3ff޿?r90$9X'NP,@5)KR/~FD4S<55JE1/C@c|#Y`FDSXIENDB`TreeLine/icons/toolbar/32x32/formatextlink.png0000644000175000017500000001237113262465526020215 0ustar dougdougPNG  IHDR szz pHYs   iCCPPhotoshop ICC profilexڍVwPӉ_zP#UDt j1DTDiJ( *"X(F@QAQ)*E.Q޻7׷. N d8Z=>@@D` l#S?d:5dZ7\&}j!>hko[@O & tPb8;<o?JnpȦLs`nč2=h>K g'H,e ܉KwBgx: &l,.`-^)b׊TQFğ3BP87h)g-,,'8DDooY&'"N~#K2G*^:V,k-$-?и3W1TIO_gW!UE9ZuƢfVNa]7=~V'u65h2d)}12#Iu fO}- Ee崕Uմ5eY[+B;W&{ }b89Ӣˬ5wn^ށ> w(jW=˩Ma&ttd-|菈c#_=y=u1togn݈/8{9!܅IqIRSi/RҼ2gfg쾤vY\dUk  ypbK% eS7'o|Jֽ%y5F= #׻4X?2|d[yZP,`[6v/ʯ^tһRTujO{ax@`ѯFF׿loȌlBRֲ-uE6 . ! nH2@9"QPmy$}݀^`0):8(I*XI6ۆ]ihQN)N?".yPjUns4<k%k*ܓWiPfCQCs# H:j4?LLBLn H4R)4),Ѭ\׼|Ƃlon* >~GSNY7gWw/_]A!Ũޡ1H3рHh gbbmOWr?!bZQ7> 3vw浮Bz2ΛշU4U2dI5z8_UM^-g-[GOQZ]Ǟޙ0S ׆>W~n>>>ɞJ:{s^cwia9iEfq݇ظwh,@NCce|z<  \Ѐ> h=B")Fqbt>Ɗ5sr8 q"z LVy)wP~vgrjF]G\~ \&fzFm޸vdđ!T6;|GYTĉS+g%jNȆ_Er ̽:GoxJ+mŻ7}|=]6{)iy|U|]p0H.#G^Qt4)ƢsgTp'^:,.]M@Kv'`eMzfvY=yOfH",ڭemS( wenXe%yjtx6rq餸$Ԋt-y 4Jʧ*k>2|znБ-]臇c_M~yW ~ (=]\&1%B to fa H-m[{Ŵ$!á$Iڝex4ojeV!1޹ĵmS[דK4qfO2-$R?ppلk^ \J[vIfUEKr+nW㻯WC-zdrɚh ~1* U|1hg(׷~4ǵ56`-ɔH 125ďAH` D@P! @ ! ?S̀H&) ftjъF٥DTWU+ (~ױ$EgAMAhV cHRMms[+|(F4|p tIDATxڤWk\e~}gΜ3ss۝eZVJ-"^((("1DD_PIH@L XnKٺmfٝ˹~?zݪhN{Nry}ٿ?!% ~ÔOrrΏB`@뮳FGGߏZL L0|sp8%Yݾ}qq۶B.p,S*@ eJpX,LedYLJ'&&~9_0q zK)K c2\EVmB" \nĉۃ:뺛u]ZSSКMlڴ MB)m(/ O,˺>I9B0;;$)_n#MD>X ^fsc4e@$LMf8 W@ a,|^o0k'( iBfffz80 `)Bȁx<#V E4h ۶ᗤ$A7n;M7u]E7o&k֬߿5TP-Ν;#R}#t ÀiH&BU/HPGEY~p][MSHYA<Bs~vK  ѨJ}:NT*EǥP(mN.m&Tl^7dX- i>~ڵĶmhr 9!H&PUd2dzo{[ZuDkDU?P`0x_0L@<]sd2<AX@dgnnN]~=b4Ӯ[;ߝ!.<EJ%e!%4m;ceYmnuu LNh_pISD"H|LciX~9Ɂ@N0.fkjJZvvqοoY577AKk7>r)< 8XMO gch 5+(a;ѲumDIYPiKG'\\90KOA^Xq&\Il m:c sٮYt.ovz<sЃ_<8 GXgB-QysM? w {Wrǎ+)clVYEb *:ɣG]TLt"\8% ze?kv2BV_~U zt:t&EҒ[ ma)/q>CA#R\z/ҞV%yVAoeRP4E__D"KjSZ8Hjy e5 0-L„ 24D8,-I=JB !";xb}q lP+q%R%Ԋ"bH[|[y=yh4D( M# \BB޹ 0jCYelF$?l{5~ңis$ 'LJ[,˂0 SS3T:skT(&ڗ,lڼ&YXqؽH&zlҳ 355UFogVVXgz}ɓj_}gFإlApPkcďY:឵-[GFFbh4BB4z^oٳ*|5"A#t^坻Z95;"IENDB`TreeLine/icons/toolbar/32x32/editpastechild.png0000644000175000017500000000277213262465526020320 0ustar dougdougPNG  IHDR szzbKGD{y pHYs  tIME 4qIDATXíil;ә2L.RP(`ȢE+%Bpዉ\nn*\hͽ4h$ 5t,mg:Ys0lI·yy9s^ o1-[ 6m!Ӭ.Y+rǰ_,aKUSPnPTxpwvU`oQ >t3zp8NKh4J/$-7J?=5FTG{|<N'0̜7T"sfsetj-Wt.ZkO-2[w22O"mlX=ǩ#b0ƲRp4{?:Pv͟CLW-$9~(˼ '.&LI' g=cghfqiM|PbFIs]?U`@UDsalt;5^d/8yW^.=s:(Z:yv;Y47MkmWT  " c;O=1P*`ME--u];wdQ >ūٳH&tU堾Tsfyn@c(+"*<aCŒ47Yn67ӟ `dx`Vt[Q2Ƅ@Eq[b)ITJm}w;&2Z~C}|uwe":UPnh({ch(N 6ea#[,{D)97wqgԩXV@26eu;ng2Ttu[Q ׳=,.hl!ʄc0y] DgLL:(W]=EMm \|: A X~W$TU P[#]#\`j2)y}$@2>w $eQQ)e 1Hno 8p`+1 DYPVȕK^6c%l*q.GuZTctHfMjChf؎Pm]Pf9eȻ܏Ƕm1D"Z4<:o؈eYAcҗU]ݣ4 HUyHx$F21 ]<6`$1FGq,V F 72 ++&rX)7 ٿ0k|ɭ߆exm?Y2r_LZE/{y_3=,|s8|s]DD;%Ipdx2>dlSW0-Y$1!!,[[[vtt!?xOaTrAǏ` a<.@ >-RRR=UW/-6Ða< ),,lkhh!9?&n-J;8e_.@fxV555QLܾNU[)1C6;@Cƒk9g:BB~G4.@PUUZA^iSEج[ѩwO.|wd$t|!yO.͘^Q__O{⯎\HΝ-]E}'|n$t|!yy5-@Xu#Πs3;bBk7L͝e?$t|!yy5-?cKiirZ-%{RT];Ak hI:><ȇ0~iuee% :r:5:\)TIu21Q;8#! dlݫ#Ϙo2,ڿ }.y\ ɗItC< 룢nbv#SZEli}|8%t|!y/UV_&zxO6G!tc5KIa<xwA)H U:pI*%ɣJi/2` vqG)=˫u;v`$)O'? R 9Hl@O/7!8[x/6|`RHȃ0aBup߄ gSS oo빹t%jhikͩl- (h%:.www񹣫e6L0,}b/3\ 7p7X>4tƍn vZ :m7Q咿+YQ#а̬}'''5$tgjG3@E:O察W_Tb9 sĽe7c?#Ml aeJ.TQΓ2cֳބؘC xh# |Cp1)N|E(iUEXb2Z"BWnWzb9XnV@3Y4%흥."gΓrL˿ѫ収c"̇6a0(2!JC.*] Zy1aΰ_ Yf9*JuUQ2 K9[!kR@1kc1%c*cc:c89=R_qb}N*J[CG-iZRΰ 'q"z$6]QJVGVzke9JwWp>"Nn"j%NPE6 Q;a"&z\'2d!6 C#! "&1 AnIENDB`TreeLine/icons/toolbar/32x32/formatitalicfont.png0000644000175000017500000000167013262465526020673 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  ZIDATXŗOP[nm7:6PQP"^b !y}D?_0|1N%A1x6dMprqDl]7] FNOr;< Z2xTCy[ z!q"pX `(稦yGUp֖kl*"ej'\W>Ui(.~'·]q+?VCȣ)nRPm4غ_ q 4?`;UJ&}EAb ;TiLW:#J\>7<<+`k f]mJIJ?ٮO{NǓD~yDW.ߪW`f$VNh4+lsM:PZ*=J&Bt#J67s.chI/t*jC>d泊V?%-^'q}+@OO5>~ inhh轮܇+u2; hkD+8}XNss4?4B!6k?fijT(L P{tGn`^K%FFVtNg]hqUVFK_?G*Քg_ ݞ 8IW<=nUkϲfmWKeR2 \=Q,JI#M]W4RQBUPUT B O}J==~zwX7]O _GX…QDz_{uEB03 qB0D@ʊ #3RQ CgPєf5:0_b*`̌\]8K\ii;>.!"x <6S\1Uj3`  #1s | :T% Tq)F%Y?E8@fjN ꔎ:/Ih& mmm;vňdz0U Ff dlt ItDUhTQDY(7uR>$1+LԒB7>̥q.^H>'HuV>n#IIطo{e=Ԍֵ\3hm,ZLsK3bs е]%)c4CCC47]K"T`X98E >[o¹דH3dYFǪsynh$k99TPYI6-Xّsa4\*B}Aϝ?KaֳL-xK$Oo嗀R-f~Fa(SU>GІ5ke4dj\E7T7X*JC~;c},x0 o: Rӄb8=LE(NClP5ڼ_(ze D-u)7{_t;-^cY8?pZ]Zݛ>yz :o|?}#&dĴ0uo@ÂlN9IG 3)MuSkCCglj-rm+3䫢i&$]Ra IU*cy8dH4\!BbR:w (e^zr`SmR1^,bXp䪎CWMg aQ̯YHKnz`Av|K?I߂>[ZD_?[#M|5^zx^ hIyxgOPAUd gk%SLx J :DT*W;WK-˾LV6.fe]vM/"fx0::SGjq*3Wz>4E˻^[~?3+WBsK3PXjUyxIc8٤8&̓ճ- Tm?ƕy'TA(foh㘣&7o0eޟEB|y^`F>g`Ο?79dQS3٫P2}{x4vhZ#wTR϶OZwgkfO0YDf>UlcK{`l]_(1 +u IENDB`TreeLine/icons/toolbar/32x32/viewshowdescend.png0000644000175000017500000000034113262465526020521 0ustar dougdougPNG  IHDR szzbKGDC pHYs  tIME ./knIDATx C҅tGE߉p!q"u(jmaJl[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%N'EnH`WWݺ> 4 D2bjv;]yH#0wxϗ)V4JG#$HR#ZsIhK#)_ YءkmEZkrS%Ɖ+Nٶ!(\}n\OB̓iխ ܶ \GP)6~uk;5+lbK o|bfB h! \,ܥgF3 j睷|̑3~F^-t-<&m6#u._k Hae[w*2yfKN`QEI#;Z ]4!% ܒ;o/y[34귁#R g!4ߞa/.八|ًRK"ͪ>Bmf6GkC@`oZr#piLɹ5_a ̌SɘφKtw1#ҁHKӲ ˇ8 MpDc)[HHc!:'ux|X)j~ļBIx cȘ!Sń\k YS !=Bh;4:>iy6pҹ2cW]RJTHU)i&Gx>ʋ?R@ItBiQ u;'$qf hD SaÒ°XWL\+W>>*0=y-9\D.7\ -hXLϔqGOI+@ 2JSOGj2Uܦ-$ 3QRs%8}v2@=dw[q-&:m]Ҏ"Ma*2R.uu(0@>ï^{JJ*!Q"2Ʀ >GH)0Mu"RbZ;Wʜ=vvJIm$[鏃fv٦P H*W# #e(tg0XnӔA>XT28zKc(P&٦ܿpەiAДX%vl2Y%drFR C]uFh;xx+$g{U^P2iS)[yӑ, ܙ ?^9Οu~[R@GaŗV.OY n}HZe;8Q²%YGAQ,י(UܱS{܎$亚7]˿ˍ k@YٞL[HWLПhԦ ZLH\%ߣ ԰xM) y%Ow8%0`|G PH|TrIENDB`TreeLine/icons/toolbar/32x32/viewbreadcrumb.png0000644000175000017500000000244413262465526020327 0ustar dougdougPNG  IHDR szzIDATx^[l9gnmi c 6QMLHH Fjb^Qh}Hj!o1F hZ 9縝P!J wf<TÃPZױmm]gp͎Nkk-$_Qo{G'eyÉhi]=RݣڱrEdXgVjS#'Ve Ҝ8s%xz",^Vư @ۼ.Fiy $!$}a1Jj:dC/dbք?T@ҹGQ]HB֭,(MK<9,hn MUM/(%h4Ƒؐ& 3{_,ckStE9ZXywBxt"@@x$BIXᐛql6p-lXB:xG,CE"CcA)M<7y2Ҕ;.\EJ._lX ߇c7mRu18n !?Ohy$Rt(_]Sn1#ąDPĥk hecpԏY!0EEN8K`UB u_1δԛTRlSCpD".T%:{oo/&Rr;4aؔ f!PwVĴLQ x1h EKyz.y+SIH/ IPȡ)_ q`gKDIo>xt$5|I@|d(h/4D) 8n*2WQ~;HcQ>ȟK̜"04S!UtqB{L3v|dsPڼ#SHuj p.EЙɛXT%9{{9ggڣŅ=fCA@=u?3 X1|yǴ8- m,7۝3#cV*6r]?aG4^)8pu>R>*9,+-e/``^~7?o%Xr{7yIdIk0,H^2UC]{)!ݦT(8\b36!HÀwD n!m@@|}]yw }J8R $>voW5O/jU:<eـvIENDB`TreeLine/icons/toolbar/32x32/viewcollapsebranch.png0000644000175000017500000000205513262465526021177 0ustar dougdougPNG  IHDR  hbKGD pHYs KY tIME$,$lNIDATxOhU?3M)I5h#SPCzKD$<  ^ ICJڸi(ZZ!&mɿμy[n6/9|~{>j޺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/editcopy.png0000644000175000017500000000154713262465526017151 0ustar dougdougPNG  IHDR szzsBIT|d pHYs   IDATXŖKLQPJmW#1JBn41>`+Ląф@ +]D" R* M{q2-0mo;D$=s{ \"l*Ώ#nI%vww}vj5f!w`3 fAa6Q[sd}ck"bdD]uՉƇo "1R,f M]@l!$cι7;[DNґaEÓ,xf%dZR:}᪃!N|Ƨ.8\0?8^!޿xcHyjab`4k5 `٦eC%;^^Q|b-`Y ywWfҵ}3r%"0̤O{sQS &0b 13q(iϚ]Y@ f@\0(KzV 0N]''6  -b-w|5q+.ۇљ|Ҳz ,q 8NMb_">@} WR5M,`  @>FK}{ B&?ijD G2ڈϤ?=lY:* NԜݥ*ۮc_me``ߋ&j̄b Z&:!+D"AE`3G:ZrΏ6k|UD,G elJ_IENDB`TreeLine/icons/toolbar/32x32/fileprintpreview.png0000644000175000017500000000425213262465526020723 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<P H4R#B+{L-PA uB!c&Vǩxx<˛._fe Y8_^{s=3'ILN|_hC&ߺ-FH~OY߼p E]qߣ_duG "\FEjd+푅J'731} ^~tЁ# ;)"AO0WuI$̩&p#BM0o^MdEyc#u[P~oOQzv~n  `ó}P-G(YExa[}Vj_P\G7~d_MmqZ֦4\κюGiuqg`4&z,{b0U2L{?__p7R͵-Z0G>x?|(+0r&G~,.Ycހ^גܻFri':Ыl<9`DsK{h|,HXz[7vIW~"Hr*q %AT[þ-}[Zճ88t{s*&`o!kU6>"`PSot=ݿo lk4X}_Ba\,&(dp]C5 7xþ^pY3~ b"X:Ev ̓>B&" ^ pfNaNL z Ypv_p1o"#ڢ nfϓ47㈷BP(dS*Lj$' מ Xd?>4rhG~9 lzR7-vJ/Ŗj :Nヘz԰x5_"OW'fj\H5r9QX"B p2qq7>L&G(kjZ/v*.ӌJ1EVvhR҉#DRHKX1k+<鑝m{:@;0"y>i9<0t@4cY>i (fZ/} E9#;st2]W{|0*UF^CO9p1ThY-=`OGS`Vf”vJu*y>}U * gVWT rpY 1h"fIENDB`TreeLine/icons/toolbar/32x32/nodeunindent.png0000644000175000017500000000307613262465526020022 0ustar dougdougPNG  IHDR szzbKGD pHYs : :dJtIME - mIDATXí]]k3_8ihҪAXSB+HQi)`P@(OmAPHEЈCklpFJ0Nb':jM&3ܳ{vZsgY^GTu{/# A$֒-*9>ID!%t7)?"4.|&͉ *!W=p.ٲ=Ny>Y{G* Gb#mMNh+_u? oQ6 @5Z`9pk^C2T"9y X`xjG H7yZNd5y\wxrE׋޻db2*av}oǏ=9O>)>9;]*O[vydBQ!H:f50U> G6o|2U$q^7 x]5J]U>|̫\D>T@Tύqۺii۵=7mȢ?MGIs!g+MK Xr 44x2gRm'X i&p~%lЃ1&sNC"BDoױUTPxW>]*1q,ʟྍ?W0b#_Nk(-IE?}D.Dr~z`s\5t3CSkxunxᖏOq ?q`^C7cOj8^rJ\_10S.֓րd]Y4L !C ;N pbanpӁq tSL,4G&SI߼#i#  _xHCl*/=MWh Qe*<)}uR` n i!؃]>BC+B*T"@j~k[!Sf" Q;l+:W\P;G!\\Y0@$'(c>AlzTS_u?aiȰ^7}@YieneeMk坥@^HCY%S=3@ّ ?U^.~̿&Tf+LȵY=Gn,o_/-4iL[9RCt4=Ӷ5j:Μ]pLKOTZL5|%jjQ)em}_ﭣ?1<З8Wu@&д[&q[KM__#& 5Z`:f#?_zJQ_%$Bb=87U,$9 [75קaA ésk*B9,yݺ ܿ7IENDB`TreeLine/icons/toolbar/32x32/editpasteplain.png0000644000175000017500000000211513262465526020327 0ustar dougdougPNG  IHDR szzbKGD pHYs  tIME --vIDATXõ]LU36l׶J1Ei| y^Lć-oM4јD|PS51$X*_.,,00 ;.0I{?{GcXO)KGù>bow`d|]! #YƠ mv:D/Iu w@:*фP怫'ϖUVB%%D~x\uujD)U15MS"/_ p1֒ʚ*֒+E#TWiiE~4XH,[?9 5,EI& J6#3H|6aڪ\EӘJp1wbs'fM>c qMB랲ʙ;P"q`Ě9e ba9k J%-:c_v5c\nm;곧ffnd 67wc{v~<\\v6 J<և׊9"HT*1AW5{}:\p/uw6ܙՖ!3Rl9Z-,jpOš=}C]^)$E[CqT Pp3ef@vwyM1'LBp`ʹݥAn;ϛEDfwRzhΐW "6!huh(5p,K $ot@cW=60(i%N{Wn|t";p .q0rMB M,J .ӝn2P:`qF R$l:sU+9u@_/0+HűwNn@i<揩%0)q)ՅVXM.y]O}XҵFh| hbazοMb}\)+W B |?+ܼ2bMSIENDB`TreeLine/icons/toolbar/32x32/printpreviewnext.png0000644000175000017500000000315713262465526020765 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ٶQUtuHyQ!DyXZfݙ?߽{ rc1ÙUx1gE r?!WgUPgCq⫕!_08^d vxIYho -qNoGGG Ͼ$sc6p}}=6aaя\BJR-&ޱ3@ Tx YYEo%@ KfX m}.!eywGLw{rd $%@x6H/_ pyHFC} iʳ(Qsajj_Y;}sa \I@8egP"XX>r^B GA;kd)Z*Qp^SUN ~w黨kF ޮrH*|ߤ&Lhr7ÖCX={PԬ WQV.HnW@Z} {aݬM~9l$q2+ - t]C-1I@BzVG>$&9S;tEЊ*`,Iq1SE̙O6LHIoy n"v6yG1USSIU/QOT??sl*qjPesYz?IENDB`TreeLine/icons/toolbar/32x32/fileprintsetup.png0000644000175000017500000000413113262465526020376 0ustar dougdougPNG  IHDR szzbKGDC pHYsIIEIDATXŖoW٫57;$i"PIRE*"4)J $䢨B"$F[ԪDUې ']ۻ׳;3af/v68wE}y! ~R}Bʨ1 >vk\~tOn={mǂշm4FxmT6bRBw+/x'~'>m 9^3>m4ZQTfgOKǞy¶m;ZuIZ6C7ڠh_cYDcWfgf.]Տ=Hu㭌Zkh D T.8FN_plǎ4cо"}ڂ(bN"dxxڻo߷:=&#A% 3ww6-! eY|X4N&2S;uY8l,PO,R `'M>?Ύ3#G,3w4kA6ɛ1ZJ.gaaCD΂15v %Y~O-U!F%xn3gWg'DtK'No0@ftt>DC!Q lxt:x<le@y ,C:3wU6i!C̃"1@Zk vjoH!BBHݹ[Gﮐ.3:ڋ~UB@Bpm06ђ[527:H1n0qJHi!%R6!Mc}pDEwf5wm0W),*`!Ԅ![}t[wq~ﭳZ= @"g{f6t!DH2v#oAGg~zfK`%j?L6w?.""q,[s6Ph1k.}v߸7C=u煚X\\j KK;q4%>.ܼB]s{qW1D[ɚbܪot9]WHg,e2#x[g5#v ;=l+;9؏kE*µqWWpM'ZxJvsr^K3X46U#igqRT* EѢBQ_kxz.t%tEXtdate:create2012-10-06T18:48:40+02:00(%tEXtdate:modify2010-11-14T05:57:36+01:001q=4tEXtLicensehttp://creativecommons.org/licenses/GPL/2.0/ljtEXtSoftwarewww.inkscape.org<tEXtSourceGNOME Icon Theme&i tEXtSource_URLhttp://art.gnome.org/2yIENDB`TreeLine/icons/toolbar/32x32/fileopen.png0000644000175000017500000000427013262465526017126 0ustar dougdougPNG  IHDR szzgAMA7tEXtSoftwareAdobe ImageReadyqe<JIDATxb?@bb`@, *մnVdf`ϣ]@_@Z4vlg]\>b@;+ʋ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~)@ g]%4t]?FDO1!m7fj69f~7iQi"q0sdD R@p8|Ʋ$qv3Gqk@D hZD4|YD2`c"W$-0B&y|yyg`Fq0+++!Fz411!:fggRzPxX,r͝ 'j9*-"HGDvu"z;ֶ',`GRYPJܻ{ "/9nckDz0/CdjZm(NR)%7NN{KD^bO]׽Lji"z-\̗9g8i< #"DD8GbBڎGJ0P&9'|9JH8>HRHSJM'5 Wf>8΅q;@D7"d'| Uf.0b}fL߯G_`6333\\\ùf &wv6m[1v< fP)_IENDB`TreeLine/icons/toolbar/32x32/viewshowchildpane.png0000644000175000017500000000033313262465526021044 0ustar dougdougPNG  IHDR szzbKGDC pHYs  tIME 26>hIDATxA  |XUK'ˆjɒth2 lWr$"Ѥgh*@D)9R "@@@ ~;DF֑PIENDB`TreeLine/icons/toolbar/32x32/fileopensample.png0000644000175000017500000000344313262465526020331 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  IDATXMlT06!`CWEJn 2$ *unX7 B] E#)SiR*ѠIf=}śi鑮yshN/ _XW9~zjO2tvhꍍ Yj_wOuq8٩b?U;qϥA2m--l۽N[ZZv;JyD>twɓL&vM2/WtlO g/^.RJGh.1\re˗o^~ù{g}aŋS=fB85C !=z>;rvv֍zܜ{ w0@;!(JyXT2Wo۶] A:v;X]u;\Fqb㠔yw*JN8ǻQ8N>=z4 ё?cc0:C033^p!^˥QJa) eòdhzeЫ7±gϞe~u 6_5=Gg.HuE"Rz.A10ƠD 9` "@uYg;(8A;s :dXd-T*}YF`P˗KKKR~wFWoŎ "@זDW P1A<7]h#Tl˲ ֎^tSB ZĄ Ūu pTz$R倆3 Z S"籹ޏ>wM;0ofػϔoa | ֭[fAVY 0JXA@C`PϟwX)EsssCOb~@ˁ n7GJ ōo_a!RNRضݐkPrX"6(泘`x.(9>>΋U~ffƿv 45o#ΝYE<veD7iիW]ye[:K .]Bݼy弱ښwƍb67::Z\YY_^Hquu4<<' ²v7}&Խ{n߿]\\^\\raaӧO[ɓ'5 8W@Vjg^""$E1®"m@ց=@h@ ׯ{>bIENDB`TreeLine/icons/toolbar/32x32/datacopytype.png0000644000175000017500000000402613262465526020032 0ustar dougdougPNG  IHDR szzbKGD pHYs  tIME 2*&|IDATXíkq/ UD`cS45򧖦7i(Vhj/&M6MjiDEc"]vY؅{8{2&g~>;'73 *z"Lk<=R$wdӋC|vmrY֪- IΫaV?'8'tovk ߹$ϝ< z,)3L ۂ-)!}{Bcłi'IIof~cn5n`)%[ %lqH e)?O=|bϕ Wc 4mc`pch'9k.~ZokKՁ0e 0 h#A"  RM|LI>p0sc/*sഡ(hȳ -l&ҚPVB lZL%/~0p,|)%P`d0!6KaO/&0G}LBPr7 =>2.+{XH#21nhxh670oܴH hH|6}ΊumiwdT޶eЖt {H1%t_8Owlx侎-- a,r~ࡒX:Z$Ux뱌^P G9| :Wm91zDzH@ ASX27x,OWyl+^<؛g>E]Ͷ3bq(g2Fx.ԇP@Mʅg_W$4tYsh8Tm]B|8S Hf;\0.uN]G蕍> 4JV!j6cAg-5L 8 \^g36r*쨯=Kh^3D5p`0z7"GC'a(SnK6c #>Ps<~"sR`+Du`[`"cƳU@5l9JH݇O)X &oq+j:jKLEIJqUtஎٵKJ& !BTm&B:rgO5E!Pp4F<.^̮3XR j-:C)*yyI[ ,+\W{9oRrR:nz~t||]@kE%p,Gnby_^*ٖ|녷.[Rg g'qŤgJ5sR{4(i<8>? w{yd&f+/7fjF's4r١/b|v8 ktvi~-?=\Y>{v5ܫ䣑p%=m&u.#\IENDB`TreeLine/icons/toolbar/32x32/viewtitlelist.png0000644000175000017500000000251213262465526020232 0ustar dougdougPNG  IHDR szzIDATx^klUNJˣbk BE-D"h4$T1mCQ # IC--P(ݶ۝{=e3Gݛs9sn~)9cP ´BZV#@T7`iʍLb"8ީ?$:$cۛW5z=\xHo0#XuwNƯ> b\L% 1u aNln /p6i9iz*{kSgZK\"̓1AJ94QSUE<%(9q+.)` 8t]!yYxjMj@ZW0%\A(yT^LzjzDP@pToAF#… %b(Xq,{Upo }{B4Upk* tWYc ȱLU88V b选 @N6q|ZZ8XgIZJda`sڇX\ /e%|{Wm/qSI<ېkQ5/=?ed1/,U Rfg3ޏK?{B{c\XspJQ?S;·{ϛxj J@%'<+OӲApz*ȕ30"!Tq[>[k.(AO'܁b2Tww_h+ ;𫝈ܥſݜEOLԈR5>>v|k$pj.bt͞| PMYyyV̜q+Uh\(ӊxc7pi[0kf!"݀5g0GAn F4 [vA{v oJpdp.0 ƆL,dwp*JU bzݏYac.H B$W1 S橚 Ѐ͵ {3Ws@XYo9%h4y(Kc͝(\#Es@& 0rdP$k+ham$. :ۉ].G P9AlhlevNCz4Q()tp1 RB I~ kJjYte!">.IENDB`TreeLine/icons/toolbar/32x32/nodemoveup.png0000644000175000017500000000214113262465526017501 0ustar dougdougPNG  IHDR szzbKGDC pHYs ?@"tIME ;LIDATxML\Uy3t mIIMHqhb4ƨq¸~@SqE?bbH[2Z`̻x10&&=w{ 7߆ 5Sxh#>yF)f+"t>u24>H:p{k]%:Ϙ)OwIؤ'39fMx+ ۵uDߩ=>7m]o/N!>pIENDB`TreeLine/icons/toolbar/32x32/winnewwindow.png0000644000175000017500000000241113262465526020057 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/editredo.png0000644000175000017500000000222413262465526017121 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ҧ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 a w0 oՌQ!̇ا8 L7 q''| @!~DMb'jIXv-0KOn S @Q (J5k{9 :b7Es0Rf` v1St-o"`m>D|N-ﴚjQˡE8"/|BL_%Z#8N\0ĥ:G8mَF.b.<7䅺lg7yYS)/^dp_D.ۭ^NύiW<3}z'Us"ۈHZg4+)ݤ<~IA5IENDB`TreeLine/icons/toolbar/32x32/filequit.png0000644000175000017500000000311213262465526017141 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/nodemovedown.png0000644000175000017500000000175113262465526020032 0ustar dougdougPNG  IHDR szzbKGDC pHYs  d4$tIME LgvIDATxKoe5v$Z.Z%J.T?EUV ذ*A,V J&NCe<3 ׎ہ4y|C:jP-II}_!GU@r"TIN& =!@z_<~oz+" ܅*ZJEdJ\j~)8y #{mEIdFGH(.m'fѴ5k/OS-0bJLq<^W1$TS91F腈b͍nP*W?ɳ'0RT"H3I$isU>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%먿HȏN!&S^/ `];8(4S IR<[#>i :J"MspAZ@՞[ժPDRBfw@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ٶQUtuHy9g~+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/printpreviewsingle.png0000644000175000017500000000154713262465526021271 0ustar dougdougPNG  IHDR szzsRGBbKGD pHYs  tIME  (:IDATXŗ?sFw&cwIQd8v&t|@|qҋJ Xm)&pwx _6w.zߟ?ur3Čk)2fLR16޼yg/m]WPFF >|{p D^Q;A\"kD(h{|uoG<8{<~٫b0jw#'׾C4Jv0'IѸ{{UF#2VU7ML:D(F%e*;"*!chTR)*$͋Y($e]j"*{szf-~e(bmL^& _(P*N2h6[PFBF4v} UƤҢ2+Y[r3ɐl\ɄuPӄĄ2FM;Mf%ٙY<^Ʈ^Jޣfaw. ?o#m%"IENDB`TreeLine/icons/toolbar/32x32/datanodetype.png0000644000175000017500000000422113262465526020002 0ustar dougdougPNG  IHDR szzXIDATxڵ lǿid !QVjt*]JC&Vh;eղi+Z6 nPƳXJ $- l'~ﹿ{ؔeY=QYzb#sUGKvz:?+2|+Re[nƻ/TTC*Kgd͎HK@]*xSlRLfs@ǝkf,ZO\ oKn&H²j ,J/B%s m2(whx(eTEWU(^%0 jcIA?Qn{<#f{`o0`rL= ~׺K ܂!%O/fc'ܔD1~H~p`32KV\mmc9R'2gM|q.?( 覸W\I p;HsqC3UnRWQ"R\͗0$glǮ>&=:v4^gs'dN, @7pWU)ۇ&"gي//+Сa央Xسyf{*c/ۂ5'aК?lkhH~PF ` }j/z tIPFRSKk0J#[Yû!ȳqͮ8׶Jiތ"|5A'qhwm|О\4%,9  LpDެ.ۨh[ dmd/I8Z\|YExYڛez?O7 W6mUPB!D5aBƼ8qF{pd x@I҆‚mq oTu:rfE%*[T|:Yƕ)fh57{ BҍJǮqTxBM=;=&ծUku)8%ӲȢ z[]@e}M: o}AT|Za梬vܑ3+ytldxi2iv&!%2xڤ$"C!0JJAq:߃7:+#$0fi \u3j6$^3mkE*ZXMٰHEx"/0(~\U6#hK;GKФ6QYQ:x -pVӬlKkz7J>R_qx0E"HP_G>AtXi!;$=9`+5ɛ6dluM +k,&X#{'&g|BGH~ &zoZXRNfV \LHRsY *a^x-)_=EE,: `10p+rSSD,15gR zX9V p0Yx.ęa4 T3du*([O4PNÖ\CC^EP1 $52]`2S(@K(8;1 ={qֽE+ɀ0֑01(%o <1?} CmOa+CBܔ e[cGlߍHEx ^+̏?mᧈG@hP~BMY6Xh~*+e `4Hl0 r& ^}1sO#[pY &%oƁdgW/!z\Tp4|s"(Ӥۊ qL@ݤvoANgt>QXB~qmıl6& CfFugDus%K}HpIpXuq=oDS㘍6>yzQ][ C"DJi`h[WĔS'PW7k_cS8;17|O8p m6%m+ě5 +IM߬8T}6ZgҿrLWF'C$'wh$}P2@M6Cz4Z x&*O u TB[a/f́$&9} ּ%QQ@tdt~?66E>H6f5+v!=Y9y;0m ]dwDaAS)0~~B@:ڄ11qb U`e>`p[akRIENDB`TreeLine/icons/toolbar/32x32/fileexport.png0000644000175000017500000000173713262465526017513 0ustar dougdougPNG  IHDR szzbKGD pHYs  tIME +lIDATXŗOh#eLff۴P 桺T/E"բ ' TzZa*PaW=Tm-vC%ۦ4Ӳ/|x8F+SU!!PJ333_MOOydzRV+yl6K>V=;?? R8!x7Ǐ@k"Vje``&&&#7$L\.G:+ cb3 dxx!===r9l&T*qe8ܥ)L&3ncLl]A@w t'lێք'j'Zi:)ROD{V+aDWzkz$+>,%-7O@H%֚[ zt~OYRGwpee%~W*Xnۙ CʛXFECỵ`xèڍ[^Ƀi,;rI{=5|SR6D)Ⲿ+]{3v%52TGN#%kk[ ;>2 Pݖ$Y^[Zr焑XnmuXbRJw,J:@`-;3ŭ?62xdk\iFQ plGXXX\cOD?;"Hѹ"S9FFFġa'0@nk|< sD`fGpz]J5RX,RmXu"ϋŢPJe}߯,---&w}Kf0͆j~[@jw#oK숾fR}KƘƘQk%NVQ97 vG?Ә9kIENDB`TreeLine/icons/toolbar/32x32/helphome.png0000644000175000017500000000331013262465526017120 0ustar dougdougPNG  IHDR szzbKGD pHYs ,tIME  3VUIDATxŗo?sf粻wqbƉb'M=-Rڢ h6}X>BJxmQʦ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/nodemovefirst.png0000644000175000017500000000220013262465526020200 0ustar dougdougPNG  IHDR szzbKGD pHYs %xftIME246 IDATxV]he=󳳛jTZ(D"BA%`PP,⃾Ciik4(VmjbC4;;3NY%t7䃳~3sϹ[k ~J1* <9={,~*!_JTyc= xԘzfe,f2G؁  ;ϓc\+0/NOyh[;w`G;hpTAGwpN({e#zҧ6eSwOU1ѹvrѠTB/;zfYXح2cG?r~v%\sM(`0uH*8rEiF}grXPƻBRS0ht3Wqu +nlh2Lq1YN M/%+ȱ]ԱmR7j`4'T8-.D_zMz'_ҷ6/nqnv69hftڰDZPW0:ݚKDM}%I |7fvcutK#,"op!(,3#6=aO?.5GD!D7F*|K집=lW™620+X0QIyRE6jGD29ψ I91K|mWr&v%d`S\i>@\:^ǩ?,8v{lF/dTo?mFYifqX"` &{WX[$ÕDMM WY%fu2tʵ1Z6=Mm"|զ$AV \#a{[ߧh\~iVEPSt3j?]z' "@Q PlAeK1 4{zs:FKF}7?؜K!:yMw>DDE;u?Eb<8ܲ(y zZ2D9u53X;(Ab(}`D\kIl^ZfM yHLhȦd'=hh%[7 [OZJqAE\W~d4/U^ڷjqt+-&)I>ipQP3겂inBշ8tuQ$YmCFӖGi.^lLV6#;p= )CzUXMˣ:l?E(i)Ǯ/pm~]* 6EjP&  2XPҿQ`۩I7 ˜,Z4T[lEQިG@Uѓv#*!4!S(

    eSwOsy$,0 (8\$ x̢zdB0&mRUk3j ED׿J28%\[/@*3W* |[QH4}Ա%^R<+J6j;6N2&IENDB`TreeLine/icons/toolbar/32x32/filesaveas.png0000644000175000017500000000402513262465526017445 0ustar dougdougPNG  IHDR szzgAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?5)ՙ~}x3e7~^?wgOH/@Q2@vCO/@Ba#_FF?1AՀhY x𥜁V?O{ΧA@_> 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/nodeinsertbefore.png0000644000175000017500000000171013262465526020656 0ustar dougdougPNG  IHDR szzbKGD pHYs  tIME  ,2 UIDATXŖKTq?;%3jQ>T- !qz F Dj?af=!4uxZxG^8trùs|w@)%RJ)Yr诽|9|] . ㉄@$1ҳ)럷j1aJ[@ s'ԀL P6"&Pk ?TH|ت[ݴ~/rpVl upzR'E qĦG0e655xhhhA/();Po-y $e>d8{>x2TRAq!Pi;<ӰfȜ /-@Pc`]koo:rEvuat95'H2rtty&tSwҪt2.7q |'zj3$uG#.u]t~5E`ʪoQюW9`+TT +%??RJ5f VK!02g#mV ׯřԙ[r?(ȾETvA0==ϫWߖr<310L حAyy9i">`IŚWeaffׯC.b& pV|4 ƟI,"NيD-R _~]D@ӖSz7n{D[t@>J'}@1cPf,|S3\sy,޹hf"@F!^c:YR6CUMߐBYIҙ ʜ;_ n]й2Yse#e_<õ])nZ1Y ;6)3P@2ۊZqZ-]߻eͫ`Je\ ~D׌}u>- 6~# &1ࢻv$E_:Ӳ,rHqubxhț!' aZ91V-w癷fV>/^ױo/w}sX_k+llYs%}'؀b9W+qOft_;^@džggΚ'\:#7*B-C.(b6,8c j0cn`bcpx(e 1%'|j}-xڱL"\[KdNGYH6z"vm4fL RI,˦PU#;Ml[g(}J?GҵC5?PUbF+ B2s P4BvT},vRw6 QnC!Q>"e_r?i ǎ1Жsc娍4DQA:x $Z+šf_{jV:6<َu 1R']FkTi#q'֔/~}aB݇e-;+,~T6*Y6c=ԷƞO~N` !k>0 -c1fߜwGE 3GwNIENDB`TreeLine/icons/toolbar/32x32/toolsfindcondition.png0000644000175000017500000000251013262465526021230 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  IDATXklUϼ}lRHHV-iB P1&JRQBl (6!~XEB#A [vwg-t[h3shΝ-ņ a FPtRNQD93&m4fR砖׻5F/|rj"GQtJ b3XE({aTkĸ$vVNSb*#bmLH(+Ԟծ3z+6b {[w+bAɓFd ンwF=Tt P{c&J%pFd5A$5%XF\x) !@_{J _e'o>Md۞oW%1ʙ@BO9_Njyan  3t-jBVeeG)V j<ѿ@m}vVJmhIENDB`TreeLine/icons/toolbar/32x32/toolsgenoptions.png0000644000175000017500000000356013262465526020574 0ustar dougdougPNG  IHDR szz pHYs  "IDATXkP]n.Sh$jb6a `v&-MZ;~`ژ̴&Ա%Vmf8r aa4IDp_.>v~89s{wRMݝ'E+%FMeej軂iߛN̢4JE-@bjV! ׺{F(}16B1ۢ!I;Ie||l<$vIOF% OCbjeEwV P@/crlwT/ P;졎1[t ʬWaǮsjYx\YqCҫU ͊B M4iqnxu쇎$ c!lğ"l6"d])*t:yHdD[KZ7 \هk>l iIKJ"{e.@iELMMI,觕J`˄H26~ ĹWq;)!Ho2X҂t;^q%6yְY-a)o 8aZ $<tvzzDV(nDD(2COM}hfzPMfK O3x.::K1BlPۓu 3 Ѩ] 0Lk>9)jYUW._sY%Η 3_i1Tp?QL.d`}of&^{ ib?aRt@ =&S5>9(of0=r-xsQ[]S= 8ƒ()9;*.I3+YN'P 7__>:2s&T| 0(ܼ p\VQ{- wW W=&RKLBܩ2"cnw7Q Iai<̣7PQZVJ`><^s?9ɉ o1f6ZAFgU^ԃ~8hqeq>7cf3\x">O[f`G)}eԏ`{Y# ۛYR@coIV?6ߟ?#AGCM^Bfs_iaA ̝(K%y%#q|pzD ߈s^@᳹yg|a'e7|@ Ho dggKĐdGƕKAl`M4_cs_ƭ:(\U `#!+vw'v59B"/M4Vyoef4/€p$K>S|مF|{VF[sa&9Ɵjtnq7E$ z&I9r~J}2'3 <|`0cb utQ\adqXc4:( > EUr>\UtA{]o0RV IdIENDB`TreeLine/icons/toolbar/32x32/helpabout.png0000644000175000017500000000305013262465526017303 0ustar dougdougPNG  IHDR szzsBIT|d pHYs:tEXtSoftwarewww.inkscape.org<IDATXklU3ݲݶnKˣح -c|R1!Q P!LbD >01/Pk0Q1ՂFڅ>ѝ\?,fwfM<=ϜsgF"Ο65`>P 4^b=RT_@,3߆oqU¹ZLrSk<7>xm|%?X"n6ʟ`䧆U!S" t!F{x\9}%(.{%kƝ?(6SCiü~ÿP0;6Snfq2=ϯ{9ND pu*-5Q$_@<H%{:l@Q#_úE̛n:_Wrv(3xRNN|50rwO4s1ҕ6֜|6-J^;@<4L$Z 2˒,BZy2#9Ry;oYH9b `fQ#ۉ;={XZ6#y *^JaO1Ql/C.+؂}9>;+7e_ٮJ@Yw N jmQ @& S&d3 q!n PXe,R+ J ?&±Ϸis xۍ8J\tsoiu5UUҘKWxJi&"7}VG!7M'24@dd Q1'X)CJ_u2}uqzL^a1Wʘd7ђ2 PbQJ˯կZ&S7yUD p WUUIczb=w:IENDB`TreeLine/icons/toolbar/32x32/toolsfiltercondition.png0000644000175000017500000000367213262465526021607 0ustar dougdougPNG  IHDR szzIDATx^{]U9;3-3w|̃@-Ѐ`E(S*(Q BFE$ B[!tLtνs^svnf$%JVu%k}BoyS*Zx U)' RFEteٶo;(@o4_jri(T9YBUBuAQc0WQA<BJpfL EL%@_*u% _9;F68SIe2dee-g<-(&J&&eIMUV-K xw@C&ST穞s2'2Y_x(/PKA}aKHȡ'˔GQ!Nh/|(Yp.|@&Q8 E\#.fi&URMINT5'[gˤjl-qrșwI(T1 <_K$%`N;&izۉ`fcL?)x"QȆ991 A娸0V*(JX%d "4l;L_%myf>PJ_[JF#%J<Qc3[".Mf1lOwIf5P^sm+uM,! xP7ǣr0Ô2bĚ "'1QNYޱf9v#3c_ @ ҵZxm#ek{Ps]I҉Z%n} qIL_tA ~tz 1V,>/RwU+ՙeM<OQ9iͿ< KGrMM ..1@'Rlq5H/C'^}K3gTs.D9_ߴԆ@16"s`XCh?>Dѷ[m 03 M 1'"7w4P '<ڷݶ>903޳}-7h)BJh ITnF<99ʉO̖7rܨՌsΟwrQ`U E:;nv7]yV5% z3#߿C@ǿ]m{aruA8pk~3rgQ[kXZ}3{c,?7=,k~CGS wS]Qmjw@'ٵ; ]/-xywODVO_')Di۳kvfH@EJQR(*t%Q D8zbgdɂj$`-%<`4,rz#7*7O6;{!1{P(N.|q63-Ǯ"䳰e=/ Xzv lh:ڱe  XVei˹ .: 0ʣ⎗Nu!x J[_ Q\ϭ; 0+Lynӓ[/juK>}1c=W>p9\ x')@Io9wYcz}Om-6eS}#\A$ :<;K'k V|upgvW8 FW;^R`.`2D4npt'g|O \0lӮiƿZ?LIENDB`TreeLine/icons/toolbar/32x32/toolstoolbars.png0000644000175000017500000000323013262465526020226 0ustar dougdougPNG  IHDR szz_IDATx^W{Lg~Z.X- (Al&|[4 %3,J4fdqK%qs1M6"7H` HRv7yҰD񲓼9m=yy~I5^Dnm+2#\.חestt9[BBBNΞ=pݻX`l6l2k@@#G|T~%_oo/</aaaXj CS@+(((|EZ`4 -{s b)//φرT}}}PRi{@`mVٳg4~KSS!Jf"q3̙3v-9s`YdIiaa&L"4塻w* ``  <xY'$vHd )Y97kM ò9ΝYKJJ?dۄ&NT@?SJ>\/IQE@=áhk/Q/l{{/0ϟ&$Bݻ/ILHz;xV$#*0F|[o-cPJxС:˗/^zݤFF_ 7bc_{d5_q!4=,JH(e_)V⡥ѣ3]?ieeeDEEN>j &Ďk !zxdž@LF9IBEplsKAie_^xϰǥ׃>x\` a{ 6OO?'Og2j۷o @pwg-Z3a 31P&&4 xGy@kll~(WL&AZ(H;wTZz lܸQobrz1)񠬴TQ9Xy SCزMF(pDy篿qXK|#;bzk \2PlD~}vEMM{FIȈkomʼn xV@Z,⭹ىc? )i| 6[31 @M/@jyx|4x57'햩  0FX'$Ō *!BKG_HNNn[ti_ }IENDB`TreeLine/icons/toolbar/32x32/noderename.png0000644000175000017500000000313313262465526017437 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/nodeinsertafter.png0000644000175000017500000000171013262465526020515 0ustar dougdougPNG  IHDR szzbKGD pHYs  tIME  .rUIDATXŗKhA)>J6@ԃmJEҋOz( P{A HAEPT5{(Ƭmq3㡻un d07c hB)|[S>ut(&RG@ lV>7m@+f @#PD2 gR 9(/y䮩7TV.iuC._yPffXƕ/UJs_ 2><*n@?6`͚ m[HX,QkYa?>NV9|\]RFFg-~D0Uoii)s>Do<~x$-2oz;::G? ֍Z-@Xe1:޼lٜz&AD1\;ܯ9N=6L0ǟNIz!f if`MW6P{c @3GE{7vq`JrqGIDATxڥ{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%Ė}N Kʼج2B$I1Y7(1 ]kvٚ˒aaq*BN|N),Â!T]դ{`\YVT.{H/#ONrG{᧟H)Jikj^(SDv2 i`vKRtlڃ/?LJEwh`Q5h,b”x\V=E5ͽ?|6y*,9,뙿˅͝$IAg.ce5{m,%hs^BշW?=w]^e<FthEďo-* ۿ`G-#Ȳ_m(jgOOdA[ ؞dYEfz5H@Y)T]] s,sߦnÐM#):kYXX² {W^ߺezSd0[taD*۶~ՅBKmC83yrMΟ(b9řߙ04c0QS'OƮ,4r7ɻ&u.=G^*"$iuOFn@s(`<ۛNAY+Y_kKqqySq8pڈCA4ՄqUĦ p9.K 6,H)o Y)w!6Ea(]4C$E¡]& [ 4$J0Al*:Gkof8ˆvø]$)P$ 4uHD ˕ >2D8ތ"0*75kxЍ_O eۄmldPuiHqSA:2IÀmR@4D^I[/6G"[+[x3#}$geMYp5_U )!]hni(5kVAG%,vAWNr gN85c̫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`| / H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-gAMA|Q cHRMz%u0`:o_F $IDATxڼ{pT?M͋#y@B<*(P-؀!}G+hg:֎NFPТ@ $lB{?Dvw;199UUk5U*KPkcE@pgMUetԉ?;WUaփOcCdx\ lJ*@`#9`+{i+rur\jIW?:Ub0X PSUv/V֗?jXvo K bѺk}/JeuOX-S5U{ihi??7)qHL^9ݐ~@6jKZ oP}h63Zms'&;.ml-`/м%jK+pJO0=D$u EM̯mY_l|~6cRc3Yo@p7aF0| ʶXu/teO1g$UQͬenzk;8|N-;{פքk{i@G'NuQIZ=߆F#PuvdV.{G@4xM[OXy)Z8;ZY[;pԚnKYgN0C||Ot|ܮ'=1nHⅪ6cɿPfhY8V=w}̹bzrqhATQlߕl49QbKAZf䙑Eo|E~E$Zudt(N6 [(fXVx 6vn_ )INa;VHVFWܗ.SdkEN}c>&hOHQL__/㬠O`_cnp-y7ٖt61'$o(*C}ܛs 6\! Τ%ϝ6{YpJ$w~;a(Kn`f+HVL^t{&]7`/v:4t I8Ҝ:ob(Ĩx .OGG sZA@D4s*pu AEKAq&Niu2ߴ%ife7̴oQ$ch?)f,iN!$ AP֐ w|0*t{XSr|8ܢ`P& sG@.Og%GƪVAQAnTjd&nE+,ʾH{C ~dnp}FnfHf$B3q!?Mg{}[S>JgWw+*Mtr(:bT(0+,jXS& .-D"{mk@Th:wUVYChn@P.'Fi@.Co}47" `7Gkłp-"prKH2Ն:koL =/ EѼz &a|&AGw%/Pm{j> @ 02=Al{ACAF<CP? 9t 7&؄u;eQG@yMHKK+1YH-Tړd1&}26}ymbׁV{D<==W[[[)33V^[rN>6>?Xo''{yYPOB䟡$Km|!0b%[677Sniuf'RąX)%JCnaqGQ  FoqnII e9dikα]1+E]wdhaÏ8x#zBc¸ꆆJ>6!xt-lE2yGC]Ckrssoі Lﴴê履EUl|!uPu 0AiiiOUU=3kÒUY.E.:B>)r ~e|A=5T0F\yy&8p~+ht`+kI-lyGQPP>>#&28XXXx oTVVRSKwͅ{-/g~ܲ ?◛[nknn~0^4&-0a,:w#??۩jSW6^///$zQAEIn*J_y*X4}m3|$ĠW32#A 3/( QcU2_Mj*XyˁKjC%0scѹɀ"^=/3f0l3`(EqRIEjb'WhԯjW8AC8  Y;1E H47OgLcX/06CsaOS=^#1k\nq. IENDB`TreeLine/icons/toolbar/32x32/formatboldfont.png0000644000175000017500000000252713262465526020350 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  IDATXŗklSeǟsov]/0&lLA2DW)nU~j5ht?H 1QH`^l&R.9vkwm=}['99}w=5{~tҲH(&>7dHFA( XyȀ>9 ~|1vIz0f+'7jb c5K̒~aLooog&_HRB/z^[QM%[v'(@ፐsDFiT«͛+4 fz4ҭl(]zɠ~0F#Z @u͚ E`cc=SU o:7~uLM,-i'O͟z_U_ŏ;\{<-0ח2v2ri%.c-dc^Wĉ1-}`9pZREȼG{[\.E29Ze4}6g<׏{T%;r:lɁǛp/768nemmr;.[܂ԺDXZ/F2U ꬗e 3eʶŒ@# sI֯i@@ ++Ȋ-)2nٹ{}` `!xbhA+VV+/|ց߆dV'zuVA,*˂A\/K>H^KKK*=VVo.O%í?x珗t[06MQ}`_߼y>]oAssmVv?8rۂZo[u[:LWsCw= !9Tind׊ B z4m*HP8sU(آcdZ_30+A[ň(631s,Je@'oOUj]wn@3P4L̹Tk믟\ݰk-ոJP@ھZn7;njD$ynaAE˻+4|Y§P}9_NP!0'( C_!FQk98`$RriaKn`1qmqugKe& itdk@3)K$ۆ‘oYJznf &mha?V90M"K'} 0 -?D/ڱIENDB`TreeLine/icons/toolbar/32x32/editpasteclonechild.png0000644000175000017500000000276613262465526021344 0ustar dougdougPNG  IHDR szzbKGD{y pHYs  tIME *6IDATXíkleۮ-حC d#L%B/L-f1 e1D  xCb0TD 6dJv|ۧae7u~uc?'x^!y>;рaX1f֢V+EW|Ϫ[n `6Q ]{݁z±pX .0h~iܱIScD~ h;gJs0k v.ɂNGj^9o~z~ol؀hPD4H!иEme7gE˖J%p욣C!sAECߕ]S Li 73Gh!Դ&a`_>s()#Ԛ/}cPD\ن-D=Vns9( d4N<=n'f)"B6!tl>?p0Vg~[< )lyϳ0Ķm{E2+* 5^&ܜ5ݲQbEbIZZ\9x/c eS S}muUoH&mR)!7G2ee u{w޿O.9* C[U6~`l߾P(g9sǐZϥw ΞD(AHM:׸tpX,Ŵi,Z|*)W |z%,L^=V,Tӌ1(~<ˢ=*7"DGb$#;+ 'Fć D\PKuoafhX1|1@0<82cI9+L\YaQ^Z  IENDB`TreeLine/icons/toolbar/32x32/printpreviewzoomwidth.png0000644000175000017500000000360713262465526022033 0ustar dougdougPNG  IHDR szzsRGBbKGD pHYs  tIME   -?IDATXŗkl;3>lv- hx.AVZU*QWUR 4["!‰iC5kٵޙEkKW;sι9#?Be@!@W~EKbLkÇܹ_QQQfMavbǃ۷n ={S>@߳| @/սrȑݵ/J?+LLLGuU>o?vtJ)tl-Bh ^[[Դ{?J廧E_m 9MHR$ 0mmmp8ߟܱcG̵Tk7R}ļ, ݞWt:viT*щHS`*//CVzA:n0>՛|-u|)y B1Dn^` 80dn33=r(ez5tí {/50\6;;1S)gtu"0!}J';SAJ)Mʔ"$ajfS"#͙]cҦ)z|{E;X[) `!ޖ 0=b,&HFɀH:T=$\|+BW_DgJl٠ykܸ:*[e+@(sJCq#EATM33cb!@wB^.S BAxвiʹܞ.82 tuW%AV`\?::ItcO5b\k3@i2BX[dF:ɩi=z7.[$`2 A0a]@2 ֌[҄H@: %0@.Ёٌ!0IENDB`TreeLine/icons/toolbar/32x32/editpastecloneafter.png0000644000175000017500000000311113262465526021343 0ustar dougdougPNG  IHDR szzbKGD{y pHYs  tIME )$`ϽIDATXíiL\U70@) CQJ)FiZ[ZMuZ~1m5jjlFM4T?H5j꒺ƥ*j.mfya7m=If;=s*iFGg`jn7n>)1K4rǎxQ| cMf&*(2|ۛݧ@-^Polw*|^/{c0it B=rTWT;׉RZ~֮p<֌Z;]U\bTrͭKV6J */\xcpO|M폅 S|%Ytv]PPez* \8|Owe œ&oa2ʼrMJ#|K9MS& C. ;b+~GGAK!U2ck;m`ي߲: $ADm‘Q9!w[.U<&"tqJxtFv}=Z3-㧧'DiRh,xW~,uZ5ZAKK 9ASfں+.k>`ȥcQ?wvg[fi@[8R 1[[A]k޻ G8³vӖ]N(JA람َ>,ų/ȫ̥WP?a%CȔR|Cig~\k]Z۰ᑇ?'&G fqFfH,#M4@43-.˅ւiAyJCzCq䘔35`2Ftj5HnZVXMiEZ-}q 0F.CKiBiB)eYt5GT [ vbM xz9[[v՗Y\wC?e$ct H@6ֈiR{|c^Q˫!fSŲ\hg^mO0b韓;ei+k& 87+WS5ƤwBNm,1!bLO]H4(5L.ƞ S4ε_+.Mt;^:+x|zIl;RIЬP"nNLXr<-(%'X)[Lv+cU "5Dqt@@/ :] 5Qд\B$ޘa(W2'YR}^1$wϏWegd&w4n,;/Rgz:Pi 4u3ps(}^Lć3 >$ 2& !l- 2_扰`V6@@K oa6TIbo p@u!2i8{RY^n%:E##Y<`)(G/n\[e IENDB`TreeLine/icons/toolbar/32x32/viewdataeditor.png0000644000175000017500000000300013262465526020326 0ustar dougdougPNG  IHDR szzIDATxŗ_lS??b@QZ@66U"CNڤM4UӤ=mIZa^6UL*)%)i ı1Nmc;\7+#{~=^?7K;^O/A-_>}3?;|A=J\sN2B1MaPJy8'9ꞧrc85y@}%P)VB&^V7ԕ5~7l]76x301ZVLhdcaps 8@\7_MޜO\br~+ ks#<[udžib6'x~4!l/ =SӐl~]]""躎aR)p_#$0Gw?w)PJ!RqƧy$:Q[vF}>26mWl"©u `} Mm55g<$D`.8im23 i%f~@ӃV'mpvxĻ|g>BmcეJ%FM Q>_F$i"pju]Z!i6:#|zkbǎ H]&Bm׾ڳ(]fps^:@75U|.*L&C z[]|F_&t\o$PˀR>as.uwό6e`&4MZJ*m3; /$!0}$JdUmA@I޿ww3[hʋ۽PbHZK^3J{ (S}zi]ݜ|ÆsAKvqp]%4 !؅)oi΀o%Rf ar۱,J jkJ@_)d@Vd,dBnz{{3u E[s4ť \>O&0 l&;?Y#`ebj}]ױm{O68>y'o"pQNEPܹs<*"9zzj7lp35  4i-|G B5!Gggg=HЪ: jJ.㑾K`Aә-aF=VB:d on"3(H?cLOpb'f>`lpv3Ǵ:Q7pɽDѦy~F$?wyM#=!X ]o}ɶLQJ#`)Hسw;R/'>XRIՙW.Nm;躎)J"b@[h"R:KJ3a)J)Du XV%]XV_S, _EV V\ 1j JAIENDB`TreeLine/icons/toolbar/32x32/fileproperties.png0000644000175000017500000000230013262465526020351 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  bIDATXŗNXuH v T6Rd>ˬF#t1̦b7@ꢓBREMAUAq;vb'a5G\{wρYׯ_۷ׯ+o޼ ݻwx1ƕRcLn]k֚8Zk1ZklVQKzjŋ61l۞:eeY)Dߎptt}a 'ضmqLEcRJO3`YZJOhCA}zWp84Zdss~,kl2)b|WPɳ_jhVbfm/ 9{e+b< dGJ)rּߞ.[[[lllzA:Nd1777'2"ٳgc㘻;6gggS݋˲<!D.d j5 8Bpqq.N rU @k]mjF#׆0Ii#DׯH)IfD`0&}qqAZVzEU0qO)&ZfTc }R)08e!A>yH)B qlf0j:L{v:(Ð%ω8q{TUvvv H nllZj<~nKc}zNFkc$".FcaĩRF r\4C&a0 3P6Fa&vhG!( T)feH^^^fwwJ2"9"$X.8>IJ,>}ϟLB9) 3P( 1xǓ'O\XT!d,juzb|"V~P& kkk]pǏá԰1&]QO ʇ~,GGG?۶X5hL1(dD@%ծkO0}Ao= +6 f583FIENDB`TreeLine/icons/toolbar/32x32/datanumbering.png0000644000175000017500000000052013262465526020137 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  IDATXAJA@7Aϑ케s\xswAt\ Gt"ji􇆪ꢫ(*E7Y įY[2u=hKc R*M[̃ Ǹk\> O8 @^lSr}5p{@ف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/filesave.png0000644000175000017500000000250413262465526017121 0ustar dougdougPNG  IHDR szzgAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?@bb`@bAX߿0ϟ@ FШ?`5U #w.Ɉn@8`m1AFB,~M2$r ~fpr.oo#7o?E8ԿARTFp+280HJ|5`pZn.v#yA>.~.Լ_~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/treelogo.png0000644000175000017500000000305013262465526017140 0ustar dougdougPNG  IHDR szzsBIT|d pHYs:tEXtSoftwarewww.inkscape.org<IDATXklU3ݲݶnKˣح -c|R1!Q P!LbD >01/Pk0Q1ՂFڅ>ѝ\?,fwfM<=ϜsgF"Ο65`>P 4^b=RT_@,3߆oqU¹ZLrSk<7>xm|%?X"n6ʟ`䧆U!S" t!F{x\9}%(.{%kƝ?(6SCiü~ÿP0;6Snfq2=ϯ{9ND pu*-5Q$_@<H%{:l@Q#_úE̛n:_Wrv(3xRNN|50rwO4s1ҕ6֜|6-J^;@<4L$Z 2˒,BZy2#9Ry;oYH9b `fQ#ۉ;={XZ6#y *^JaO1Ql/C.+؂}9>;+7e_ٮJ@Yw N jmQ @& S&d3 q!n PXe,R+ J ?&±Ϸis xۍ8J\tsoiu5UUҘKWxJi&"7}VG!7M'24@dd Q1'X)CJ_u2}uqzL^a1Wʘd7ђ2 PbQJ˯կZ&S7yUD p WUUIczb=w:IENDB`TreeLine/icons/toolbar/32x32/nodemovelast.png0000644000175000017500000000234713262465526020030 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|-nNJHӔ[ڜRҍ(㬥#!DW!8t X,F2u]Z6RkA$B8J֖w bxT*6L4d2 J<<;0gY;V"#Rt],%6ڹ_R yA&;|'Ў/gVYv\O4cfMh\~#&'^=̉zqQ}SSZuC:T | KYEhz׹{KMCR mLf#{ФjhMR;|r{F hBFo__Ȝx@ "Z#~rcMMMVC߯5.<;'ޮ] ;Ɠ,߰m{_ʊzِR[w[X}!p^SzO`7RZ@EJLV o*+ٿHIp053IENDB`TreeLine/icons/toolbar/32x32/formatfontcolor.png0000644000175000017500000000221213262465526020535 0ustar dougdougPNG  IHDR szzQIDATx^VMh\U>͛7?I!$QKB*.\t"P ٝBPQw`) B7Z J!i'R1ƌy2K0L^&=qw~yObĎ}& `"#dx^D3 A΁[.MĤtzi, J%S8Ħ)##nH>OosP1 3F&C ~F͛?RD1%{[4;82#9e0Th"jɪVϰ lkyYe@iBawݕN[%ԩ= ɴ{y@vȱ:I[,@: b>%DLK݉Б3h+ᄑz SZaP(?t$v0Z j-:j5B]< 6J5컟 {I}@K[m;7!Q2w!̬k1)r 4HPpE"1}wOA"zH&_p"T:V.<ٲ %Q"H# \.aSC'm9 T0󬽱1P_U#s.:rww )zut!Ā~@FS?c>,mxxXZD=Pu]ܭ-0 : MM/_ IENDB`TreeLine/icons/toolbar/32x32/editundo.png0000644000175000017500000000221013262465526017130 0ustar dougdougPNG  IHDR szzbKGD pHYs  ~tIME 6*-WIDATx]h[e|Idj?E:*lex^l ^BA& pXv14(b/ʘ= \t5gҖ$K:^t3m]>>}`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/editcut.png0000644000175000017500000000305013262465526016761 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  IDATXkLWwfgawXAy#(gDQW4>5&jqA_D[6h4 >ښj1e=7xQU[UN:`^s Ā&۴Xv]4k2mZWm&Ëa7 ;^?d ^Q<6~\*"46*i!mKUnhBaP2tRm,4٣H@WyNXR^Hjf WQ$P# {rP)r8İj(Ro cBfH `XH=G cYL: u[-:$% =H2#c] Z+mfpZ7(ݕI{ud " rM!:K8 r^yc[˲ƣK&2yIENDB`TreeLine/icons/toolbar/32x32/nodeaddchild.png0000644000175000017500000000155413262465526017731 0ustar dougdougPNG  IHDR szzbKGD pHYs  tIME  ) luIDATXŖOQwh !Xi4&Rcpaܫ &4jL9W&h&Ƅ1,L4$b(/:L︘a`2ų3| yS)zw& U@:@ I` 3yYEQ T4ZZ5-{)u՜s{@945#aww%΂k2)ֳܠPދ44:_ zk=Tݤ$Etjkјp3+^nds3i7uihz~ YЃ_S,,lxLx @7K>2bl P^j '!`"1 [*V@o޽S'l@ hrO}yZ2Z^5E*Y x??=W36^z6+0>`9(qyNҞH xZ?N"LIX67gnV=f8uRRPue`q)J6)YDUHW]E-1T@![+:n5 [z7466"DƐD.HHR=t]g37t;9\F.RC}e ])_BNtB6+J~7kֳۺ4< SNnc3 @?am܍ڼw2PeT_NB$y(E+G:pY+JfL-*ӄӄZvIENDB`TreeLine/icons/toolbar/32x32/viewexpandbranch.png0000644000175000017500000000176413262465526020662 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/editpasteclonebefore.png0000644000175000017500000000317413262465526021515 0ustar dougdougPNG  IHDR szzbKGD{y pHYs  tIME ))k IDATXílg?{^ztܖv#83#q[?4Ncqud11 HdjST`a$6e-+ e?n}[zm{o $9ϓyimmmVgB޷W*Ӭv^.F b8%[L]{U㭨U4~*Q/|=d$`֪́dvi*tvq=כp8վR9|T\Lݧƈo_-6漻Y!&;W-gNX!>*ίܺy};͕N`> a&o O$R()ĆOeYYj߁3\cI}EW`ėD8{>CIC7Ȍ1v8Fn XK+C7<\`e1b 4?4̩˄'<=-q)X`Q!Bjkkmeddd^SD`eq>_ր@DJdX,㏷w*񸃪PZQ("O'*ױ%Ҿ #gPM1N#MP c ^s|cI⊒_oε!ד7 )!`⺸"*КqY|._&srrF%iQ"ĈY$/ouI"Q7e Fs!Hjc]֔Ga0oΞԗu&4U~Fïˏg1=7(Uk^&IwASq*}&Ok,*RztK9M4;WS rp7$cULibiL=l[cݪJ;*]7F Q& +@5e&Tc PH$21ͱ`uy34ĉKLMlgq(?+ SA 4޻u&Ӳ,u@+U y|kal,mi~mLS!NޏGIWd +j)Ĥ5%TS?x~9{N3v 3` LL‰E3anWx&m5lݾS'?,@\}*~[Feonx,<Yy`Eus]ݩߜX('׻8soE+Y](jIENDB`TreeLine/icons/toolbar/32x32/formatintlink.png0000644000175000017500000000345213262465526020207 0ustar dougdougPNG  IHDR szzIDATx^V{lS?^:ƁJVն, 2B2:Aihk5Uh];-PtI`L}JgMJ(qk{ξ?nS&4s^ -7no7no6o}`k|L1}?鯎Mk盯~k`7硣޴F^`Yq1;J5J1iޑ}vC gZO)w rYŊG:1/ X{4cryeWse KmSYK#j`U#(n=(:Cܕ^DںkOmgٌܹ8Fgw{'@=Κ,#ż{YT`*ʷ vTln9+=ӣo]#f^mmm; P|O >|I'O3k\{buMv^0]%4!72 wcU <ں:y~ΒP)g麝ǣQ9G4UQG'5`3_֙> _RU^xyO8BQRhQq ht0(tGDTae7%NF>aPhɫZpKdNT(7GtmǏxtTU(=#M j?a`emq10# "Q^:x! z=.V|mR^̆w8IH<7o:~N>ak)Ʀo8F3ؠBX@h40KV1'J Yt 0>;p$jp=s5603eEՇD{hqaL<`L(2s"A9b+M qƮ7iIUH)*ǐ| Z/0}U +A9NTtוֹX d̘5mޒJ eDJ^]C6MBucSd ,ⰆVomb S4>o$. F#RaBdjf`@@GVTgEQ{̍CX,@腓u$&_D- @<9+]]}]n3{CM*R1Pk-8e;*aX2ͪpbx>$5c^C*-~a%0G5.`y)J qQf5V'z]j( Gu\{7:P4/sFD#g&@<1rvXS4s"Ka~8s{<)G]'`^XS|yXLT9 ^y'tQM֜be&ׯǙ2HFg)Fcjp h>My~L1xvwrv5A_T ^…NxyIENDB`TreeLine/icons/toolbar/32x32/nodedelete.png0000644000175000017500000000233613262465526017436 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/printpreviewprevious.png0000644000175000017500000000313113262465526021653 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/toolsfonts.png0000644000175000017500000000341513262465526017537 0ustar dougdougPNG  IHDR szzgAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?@b1UUUgY,`ddLLL~b>v؁ǏW~"@8..n˗/۷ᅧ:D@<t`QL#cd1tyBYYY xyyJ@ _߿8-Fׇ {@4(^8@rĂg PP Kŀ̀!Ă0  `q0Onnn===Q-@ dǏ^~wg28qOf8uX̙3@!\D0>|#ZZZ`>ógDEE?yp-IIINNNpK &\q̷`{.@WWW~~~ښʊAVF1БJJJ`}@S@5rhX244dfx)0*1?I1a??(ӿ_`9 LL bA@ ?0< ?1 14`b+? -G g(pc`eg%@1K 1vfC@ó ܪ bJ o>B41ps4 Kb8E2T?/0#a 9 r W/|p ? e * w0ׯDع>y`(,=082 `؂[ ~f7`d~=, /eb ).30Abȷ  ?ř@ X˗/ ? oLup" ^`Е,>&?`c!@x(Osqq o߾1|A\JATL0'  & ?2O`I7#9g6e_޼y}F@ %U_-0a Ö-[n .z988j@e+Tr3|Z//×! @,riPA0ë |0" !!@,"$`50~#pAEN"(0@,c~"pk/y~g,޼9fid6@k !; wfX> X~Ú`-'( E1peo10|{`e/# YsƫCzl8 Qu'Ik3.8?,+P/q5ð9 X` 5, *%6\lBFBm@GÖ` iquiFdGv :Xki@Cg;Ć&&&0 ͰɁ," 9b)*RRR@1&x^"$*?~0&"= JU=rϟc J ЀIENDB`TreeLine/icons/toolbar/32x32/toolscolors.png0000644000175000017500000000246413635455423017711 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  IDATX[lTUtfLEZ,XJ-i@M\c )hb1"Q_ `J$H$\#hCE r,g>v:g)霵^?묽E^=^.2*UVcȋ{MA7b"i&жM7ɫ2,_=uJgeTCY_H5+b)1ltwZ+^/5 ewFSg e)@lTZPJ! ;=g.\v\_4ĤӠMZ#^OԴ[|r'b3]s(}z0DUY ].aU Ii -*Ic1.PZDP; vFvw cƇqRnB% svzCc0N]Q.7z]ye b/ t4_fbQn8G񾜴t-MtҾnݖ`v|0`fOuXlP_iәujn:3Ԅ1<z|S85ca9Yۨ ;/6O-Iq^%"sAPц3`+_Fzs-bS\9)CP@p $kMT,Yv&rF5jVZXX'lʲ=^,}foXMHjLYvy& %@ &߈ve2rT ófZS㍉a467+[3\?ۺBGԼ:OESh)6C?ՔҘu`ǎ__ү2J IENDB`TreeLine/icons/toolbar/32x32/editpasteafter.png0000644000175000017500000000310413262465526020324 0ustar dougdougPNG  IHDR szzbKGD{y pHYs  tIME  *IDATXímlSeϽ:n sf3: /4@F 8h4A?`b\|&j|C#8uFkW9~hnk7=Ϲ9?ULZ[[f*e4cݽ#WjdˊxQԆSqպJ(2|[Φ 6~E \7__V]yt@@IK˅* wOWغTD)-b=vo{l8jE-ɟXd45\dqy3J .[|mpy[ 1rO$L%, 2&8p(GFp*@ G?E)c:IHzcN "\}짌1E@WΘ4~aѿ耠EВmpjt9t=׷_tX08x@f@ ZHea94ϭ*|de4d"?ut Z3-g` Di2天3]kؿ6~9Sf޾ky5?cxg3失`mEs& oR 1 KX6֮B0{5[[UZqI` TE@F} px+Yޓ|wU>YP4h-IlB-qas#jinû` t:o#&==C qscY3ճgi&9 FhLp8h-|.Z旧4d0֭:Ό&))֥M8HD#:kitH$m`AX,-P\ybx8 (n ' M0P(p:tCT K VbM˂ y~ou7qtKa$mtH@[ֈiR<8gW][jر0 hlt:ښaĸF~i`˯3F!oV$rgkwB7=nN"$HB[ ,dg&̛ƚ)PZ m3l3ؒNb۟J)ް_xˊbh}%ny}i/:)2U1.=xZPJQ__O8NαR GzWT8ʼnh!bDqt@@}" :] 5Qв`! M#'ah+ "K$[0#F8|$W9KM`BLO/SQFHe4h$c"|䐓xq;߇DAFF©;anK bO|D' x)}WW -Ն*I<_X \|&:6vgՙ |,-uvZ{>m%vYu1+IENDB`TreeLine/icons/toolbar/32x32/viewdataoutput.png0000644000175000017500000000225313262465526020411 0ustar dougdougPNG  IHDR szzrIDATx^VMh\U8J DZJH$mō(*kڦĕ HQOI$13M&OfWMi2U n*хJM8ǽ~s9}߽ȱR]nnƴ՟۫!z- JJ]1Npv+O< kjDcJB 5Û=QJ^9Q8"{NZ)ĵӧ~ƍ!`:B"13g!5ʕ/peT*UJeryܣ5#/`yi gnqvk85ndWVtvvAD_ W$J2Fɣ~Zܩ()"Db _(VP_Z/\^D>_l&sYZ9cl,KVo 19{ eTVPA._\&LJk/̡XZ`)g.+xj4@qO7wصk$aAe6}!|"p`?(90p)6P dtoA`o*tG',c2gsy:XYL]DXjXC[Br2 [{CXS,)c -X,pDwpR 5I5o~k|>=?G0t|:/XF&S>9=MdB̥[5l ?X </{ɵ4G;#Ǫ KA( )$7Xڨ,'>[1Ax?bj׮=;fN:m 7w"_(l`ҙIfgɐKLL 336\p>4Ip` bJbs# ݻUrmχoZ\߾0b aaAasNpS4T'lN_?q9T=4IENDB`TreeLine/icons/toolbar/32x32/helpnext.png0000644000175000017500000000413213262465526017151 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:134X <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~FʟQ(Zw3>u$;ߙ} M;b}Sk~_ʕ+o*3L;nv5{0pVYA@H 0c~kYH$bI[[dի;?y^ X>6f2f۽XWY3&òKillի|qbN$܍jfkWм%cߛs_Yɖsn'Qmj p  \"ʈ  b$|ڃyH.~9{Y4FrD8R,@̯hYIK4L!J()'[xn!==*l~%OY%D^ط) &Va8ud΅y"X/: Tcn30p]۬(S^ *RewBN+O*Ԣ*X X* ȎWYh7J@ 8IU@"@9 [_j2AuhץSI*i[I>mBDJvj J3DbF!`_1 %UL*ʪɛƳ$՜aj,D gIENDB`TreeLine/icons/toolbar/32x32/nodeindent.png0000644000175000017500000000320613262465526017452 0ustar dougdougPNG  IHDR szzbKGD pHYs : :dJtIME -,}IDATXåWklUEf-}HyتU)H"DcbTM5P>c?1*QEAL4D`Ċ(y9{OouΙ3̖w骻yM>UPBCf9X9d:F^J.}ls΁R 2b@BOfFĀd'2C zJ[yx=% 3=zkkQfbc$(6B`f}8r=岑rr :^X%;WG"{DQ`bnϰ|& (^6Ӂ.1.= xEU nx7`G`ĈFN1iܹsVRXgg-㕡`7~ Ll޵%1'Bݦ"'|5rL܁Šݻ4,;97keb;lw],\n\БICd+!f @ wǸ$(ș 9hׇݎm&H@U0'!8UA>T ]Є])ƂH.ALX;%Ȝ Fᵃj!dgUQq Jj=Cy4\@>ZHOSa7*Y"avn;ܟǡd DI  M:?ln^Q<*$qQD\Í > I=ɡl>7) j\T50Yqb澴:b zU;$Oӥ{F&b"*\@Uq *c錵`0$G8q0CH$3Yn;(:#ĮXj͜#&E>9ȷ 4ne5}DL|kM))ENY"mbVQ QU-KUǗLƹKJ nhǯ}Zg3ۧꚕmNC༵O8 _>r.h1j5*cc7~4PV Ƃ,])K{i')OH7e3<z335pңuu=,6q@=ڽ̙C7Ol?Y ԅc@q}TRL?L|ݶmG k[2~&Ba F8)h} mmB[sSnW,!Ib S@='Ns$ܚV>'lG5-**V,teS"ObYr@'ߵk d YJvgjz5L7?M=>/IENDB`TreeLine/icons/toolbar/32x32/winclosewindow.png0000644000175000017500000000333613262465526020402 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/datasortnodes.png0000644000175000017500000000115213262465526020173 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/editpastebefore.png0000644000175000017500000000317513262465526020475 0ustar dougdougPNG  IHDR szzbKGD{y pHYs  tIME $m IDATXímlW8fYYMXNmWЊX~1Z[HU uUAn6ʜnҦk$M;ߞ{~Ďq9??VhFp4tXk@_0~ ӌBwidۊPՆ$+$"Ίہ2oQJX?K#0Ems@x,s0۶N~Ƕ}r?'p O3 +fZs0<2yBӑ"!a 0}<639cffS']03o?|#gLP[nB3 hSrsCBr 5Qe?·|?{w Z- 7{{V/h.pt݇~𵻦>hAD4 ",,Ɩ[s{k^"N F?O8s5y²Y LCS}{[5Z[ 9roEVwנRB,5.ֺ5}_鯨Z׾,aiSIx^JA"iai r6fQb ;܏<|~X`T*_+EujO^oU*ז E֙whوL)ŋ'icD3צy/yk4Og5EZ_Q 6s6x| X$ X׃롹4ZțȜS[ li "̭%ыq~ݷDH&h*t\l}w)VNAK(iB)$88ķG,-ܥ("oTOEXm hpp' k _g˗ [eA/0%Gk01br͈Q+J)eK?beER,Wϵl ys|w7Ɩ%bȄZghJ`iWx#+<؋|4Ix$'>D H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-gAMA|Q cHRMz%u0`:o_F*IDATxڼkl3; 0cc"$)mb i+Z4qy|hӨ-AMJT QUJ R"% 4m"bIJa1f5~ݝ)Ch>̜9ιWBPZV{*Kb|ҲNVUsG1@bqTz*?|LR tm'eUد1U% ୪z* tCuO=JOEMDJ*^TuoUe `뇅ߚÍҝpT]k14)n5eG иo㕝0>q&S`tKׅ?ȞM ~ $ӿZ3n۾CۉQB IgU4G?^b~7)9\2׽b{Ɯ'Ϝ'hn"dmS A*Q,oڽ:3r;}Mt-($h ߝ@;&=N?yBk l- fN30JTpWYۖ ޺mS}'in8K]K]Ya)-\|tOkFXhܕ-Ha5ֿ2tM |1σìtxVZhdigZ_{xqv=%2 ZxN<渿n\BFPls8#ϼwlnԕ(4__ȺV8¼y}42LI~lH(Iˆ@x˝t]ja_l%)4ijY-j!!%y*lDg__XAz"T } 5Ӛ; ]\W{#).Pnb[|,6,v`cFjzcb1D(DM>:iqs|rfi32aPDTc2Aߥ;t]\/BA"v ig I%!-wH!@tITY׵UIυ ,5Xٝ؝l22P[?F70ӊ+].jQcင$hILr2:_UհlN}R_[CJeKnoQ2 oܸ񉜜N@Q3G hoeqՖ QT ԈՄ[jͪe˖YE)wnyyy GlWƆ $Aʕ ZEFc|Y9asBu5h)ȩ#Ì=}>_5p6(X=1͡g2O2t\H߾X}sO x=1SIENDB`TreeLine/icons/toolbar/32x32/dataclonematches.png0000644000175000017500000000262213262465526020623 0ustar dougdougPNG  IHDR szzbKGD pHYs  tIME 7eIDATXíkUUo}9gT3]BJU21"ҌC')+Ԍ,OY?! + " eg^_?Ι˹,zzomtG1ZyrtMMKtŊe@<2NiRNcԁ1La&rMSm^E]\i7ŋc'N/&Iվ))<p,Eݸ4s$}}}3ii[G˗1)r$j]Qhj }~+ֆH>҂ `SWWǂyw<͊Jf4cg<|j_Xװ]߾̓^‘ ͘A[Gǘα xlB,b27ǒf*8ma7'?byqPO#444Pp=+;CAhcf%9z3_`̭|1#V ]̓=SWW="[BN~=DqgŊRa &o֚d9仞S-4i ',ϲװl0"pIs4 /XfZwY5wNo; (& F_g 5Ij5u<"βzf 0`T dfkH@@h$HD[' R'}\.[ &'3Lň#O`%d {N`/*l/J b<2D!kb3Hj߫"/死:1 .9hXgZi:FR8tsk> TG(Y#4IgyV9}GfJԈb^?d IHHHPB1>6{-Gkb)W6B቏d r6ճ6jc@@n+$" ȩzKB.aRZ@>;rB3I:B u^srQ` Ll(pe Rb=p!g$jK}hi `8 LO |qE@ f@Dpq6V {+"X&W&L øTT'RC L͙a15L"CtAhT600 h9ѽ{q8˥?{Wr>MeȔT)`H*THM!fI eD IENDB`TreeLine/icons/toolbar/32x32/toolsfindreplace.png0000644000175000017500000000371513262465526020665 0ustar dougdougPNG  IHDR szzsBIT|d pHYs  oIDATXYlTw,`<ކh,P .Cb'}H]ڼDm$pQKPDVj*ME&UH0b}gg[xlo==|sw]B4MúFQE00 #瓷MDUvNsi7p)'N:::=T AET*8`Ie|>(ܯik4MJ<o|\9͒NpԄa3%|Rl~?^7$IE}(իIKyȲ&w XJo=hp P@.6MߟCxldTd_?F3 k|d2HDP8u=4H ȒHCmUsmuF۳111hfI:a&&P+I|X,l6[Q %h B! TylhAVIg5ҪNFY?"fa`*dzzFin(U1 u@V3'UhdUT*E$!v7܆N'̇:Q5r`<|$RX"CF08_P \3+5[`>o28f1bjq@(A4!2( $vV3<|誨`jj@ @KK {l2- 4Ը1b NfrgӅ g% P0 O>]cXޫ5O<pN|l6s555>|Ud'#sR4v_u뗣 NvxIPÑs&a$>&OgV$~JgΜ\QQQmmx<^/@u Ԕ'WnIi?2s]۷\7`A׏`SpqVɚinn;N0t:QD"$IbX~QIEY$;gTVֲA&>LJQƽ5Sԝw&cSUh4liiqDQ4MC*,˫4T]|gq:'ND;!*s䟱8ן]]] [/_y޽.+mZq\l6N'n\.Nݎf#q M"6.X#:bA:_Dɭرc3Ǐt8bdd2 SσxӻQA^%S94p,'N擦 jހ&c:}Exg۟JŎ{`n2 O2~6ɚtn5kxMzkx>m۬cҚ`<1Oh'VzVMs:tr A^zNz;;|;w;3gYD'xGuJUqY2Coկ,2S-ٻ;GϘtt[/y sMMI (YΝ y wY։!5 *z?.}`JD⸴ff\76ה?p$m'd#!FV#"fhs̳|*{`(,bHFf@H;UUd +p|Sjʲ19PNEm}n!(mRw ĴHBl7|)LɺtmxW$ʹ~R1R:lYOɜ=Z9;Bu7$"?HAM.]"o n5c\ȥlO1>wNv:ΰ^9|.ҕ,u )Q硼bZ?1Hq[6?K7UQ Uq5Yj`poI @xì>|-Zz7L31Pe\e3p(*N'sƟ7W c"F b2U+%?qC?cEj)B1:^PVɔ1ī/dЋCgaVT /$C$Ua;M1BO??ENZNՃN$?oإ^m;RWםKȡ?A]9'RW׳@pAu G[Mޣ$s#TGut9֯ᥤJajBY1x>61mhCr0we7RL_3oCb=jf+m ovVć39-rqs[nxTq=ĚhP%Y۶5]g lw/3F4F';ՠR=ބC_M鬿'^s7VK#B1ىmR(KǑsO/cggv p, kG=QQjʗF$%8@`f_ΣUP&nk홚G/<`:: s5d i}F/4(l;wK&$mR\.$[5`DdqT٨H!,툏OvM*}>eYg|qNWKXU0抏[+Vط2"b/A_s&SF/Z?`9RrI62T/,pZIk0tp]fk 3PLA;5~לhT>ؿx2sdpG'젷Pw=hшm˗ˌnѥ|ԯqBc HVZo.bYgo@q]io-ڏK,q]$O tW<89 Lc']O6R:)Roj64 849GT5M:MIENDB`TreeLine/icons/treeline-icon.svg0000644000175000017500000001464013262465526015654 0ustar dougdoug image/svg+xml   TreeLine/icons/tree/0000755000175000017500000000000013262465526013330 5ustar dougdougTreeLine/icons/tree/plus.png0000644000175000017500000000057713262465526015032 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/smiley_3.png0000644000175000017500000000145313262465526015565 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/x_3.png0000644000175000017500000000157213262465526014534 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/rocket.png0000644000175000017500000000070513262465526015327 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/clock.png0000644000175000017500000000203113262465526015125 0ustar dougdougPNG  IHDRa pHYs  gAMA|Q cHRMz%u0`:o_FIDATxbd@*o*?}ߟ?^M 9{;8ß=p#_ן|O⻋`zԒW6&~6FV0ccc\@ }}P@LE) 6Ro~2|zEk@'@1XߞnYj" ʲ@00pꙇ r@g30:|*r''5O ī} "BtTp;eN =SqS { VE g&FIENDB`TreeLine/icons/tree/treeline.png0000644000175000017500000000233513262465526015650 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/(++ '.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/sum.png0000644000175000017500000000057113262465526014645 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 /YhԾ ",BKH9"@B՝'p+HU*ٌV(qF8im$˜ݒeIQ|; !\$Q=z^ r&D: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/x_1.png0000644000175000017500000000037213262465526014527 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/bell.png0000644000175000017500000000075313262465526014761 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/date_1.png0000644000175000017500000000102213262465526015166 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/MSzx q L6Z&tR a$S3P0+ \@rLqf"<8iIENDB`TreeLine/icons/tree/home.png0000644000175000017500000000114013262465526014762 0ustar dougdougPNG  IHDRagAMA abKGD pHYs  d_tIME +JIDATxڭORQW^N6J0-$$,D=V~9Юf1-Z-\q 炚s\?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/gnu.png0000644000175000017500000000042013262465526014623 0ustar dougdougPNG  IHDR]RgAMA aPLTEÀXXX000I>tRNS@fbKGDH pHYs  ~tIME&  aIDATx-MA 0 KV Y#d;l)IC"*WqUwNn@LK3Gp8x)dSvl\?|]/Lk@IENDB`TreeLine/icons/tree/term.png0000644000175000017500000000077613262465526015017 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ݧ'tZrr㏘`__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/warning.png0000644000175000017500000000145113262465526015504 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/anchor.png0000644000175000017500000000063213262465526015311 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/note.png0000644000175000017500000000102613262465526015002 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].WzozyX_\}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 #!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/lock_1.png0000644000175000017500000000133113262465526015204 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/mail.png0000644000175000017500000000151313262465526014760 0ustar dougdougPNG  IHDRaIDATxڅSMh\U=7ɛ̏3d^Lh bK!⦴lD(fh7PA20)E:4M4y̤fef޼h,spa94exyIƿ/\1,SCC}/vJeKKt~ż&%Y5С2tJ8ՉLOԧ:HXvp&3HPy oGn. }`33??A^`Dܶcu 焵 0Pt)WT~{ʕJ&9pm@J uPHHvHiB}ưu5Juxу6R R08D^$FDR='%L&Tʂq,,1?_D@a 1_˴`:?W2U.׿u3xtLS _]-_v$R9XHngƍSF}?@17(.nv1v. `jbqViΝOkޭS_Ov,f|W)W_Q IENDB`TreeLine/icons/tree/x_2.png0000644000175000017500000000146413262465526014533 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/smiley_1.png0000644000175000017500000000150513262465526015561 0ustar dougdougPNG  IHDRabKGDIDATx}mHw?keVW텒T9j!aE[kiz5֠A/6VfEO(9{_?zjs|0pX3q88{  >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_5.png0000644000175000017500000000144313262465526015566 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/heart.png0000644000175000017500000000103013262465526015133 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/arrow_4.png0000644000175000017500000000044213262465526015413 0ustar dougdougPNG  IHDRaIDATxc`ORC2 cNCl b5ـ'0󏁙a?~ð) ?>gjmXY l, c/É_ LLLp9Yԥ|*ýÂl#}|7@_߻pmR P Ï/04q`Ռ-bԎZ %*.M^xoݜ IENDB`TreeLine/icons/tree/mag.png0000644000175000017500000000145713262465526014611 0ustar dougdougPNG  IHDRabKGD pHYs  tIME %;DIDATxmKHTq93י;(5%hIndP$UPѢ]EhX M(BaP9(3}}:$I(8B6,ƽb?"7ߧɲpl [_6Wn[Ho%k/mCT"bB>_"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/bookmark.png0000644000175000017500000000035413262465526015645 0ustar dougdougPNG  IHDRRgAMA aPLTEXXX000wtRNS@6:bKGDH pHYs  d_tIME  A\CIDATxc` `46VVV@@$2A@DH 3Tuu  f'?1if3IENDB`TreeLine/icons/tree/folder_3.png0000644000175000017500000000114613262465526015535 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<|rfQzDl ^qDrq60v3|#@pC BkM0PUUV$ ,¢kRJQ;'(ж-y8(rJ!Iq~;Ƙ}4M7n^rɖ|8"(]g7珸Ak}ЁXW!a-i(Ng xn]D ~IENDB`TreeLine/icons/tree/hand.png0000644000175000017500000000124313262465526014750 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 5IDATxmmHSQ7qHm 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$

    eVBl"#Ya@Ņ VHUĂ H(gAZU\8ܧ}zy&j9R<:OHɽH gyx~t?op.$P&W " R.TSd ly|B" I>ةآ(G$@`UR,@".Y2GvX@`B, 8C L0ҿ_pH˕͗K3w!lBa)f "#HL 8?flŢko">!N_puk[Vh]3 Z zy8@P< %b0>3o~@zq@qanvRB1n#Dž)4\,XP"MyRD!ɕ2 w ONl~Xv@~- g42y@+͗\LD*A aD@ $<B AT:18 \p` Aa!:b""aH4 Q"rBj]H#-r9\@ 2G1Qu@Ơst4]k=Kut}c1fa\E`X&cX5V5cX7va$^lGXLXC%#W 1'"O%zxb:XF&!!%^'_H$ɒN !%2I IkHH-S>iL&m O:ňL $RJ5e?2BQͩ:ZImvP/S4u%͛Cˤ-Кigih/t ݃EЗkw Hb(k{/LӗT02goUX**|:V~TUsU?y TU^V}FUP թU6RwRPQ__c FHTc!2eXBrV,kMb[Lvv/{LSCsfffqƱ9ٜJ! {--?-jf~7zھbrup@,:m:u 6Qu>cy Gm7046l18c̐ckihhI'&g5x>fob4ekVyVV׬I\,mWlPW :˶vm))Sn1 9a%m;t;|rtuvlp4éĩWggs5KvSmnz˕ҵܭm=}M.]=AXq㝧/^v^Y^O&0m[{`:>=e>>z"=#~~~;yN`k5/ >B Yroc3g,Z0&L~oL̶Gli})*2.QStqt,֬Yg񏩌;jrvgjlRlc웸xEt$ =sl3Ttcܢ˞w|/9%bKGD pHYs.#.#x?vtIME 5)Zk6`IDAT8˥=hSQ_mS$\Fp4X7/*Aҭ drt fu .)h34$סI4-}ss=fέgK,=k܍YJmrnP`[)޵Pe='xI|R;b_d < 3=AX,0aA/?j+$$OIRqclA\. @zG K,@PHO@>CVsZ<)k;q$#ŏS{Q-x]>N!8d|z:3S  xMMﻗϤ*sXdI<IENDB`TreeLine/icons/tree/arrow_2.png0000644000175000017500000000046113262465526015412 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@emHk)'VBjs֋쩨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/bulb.png0000644000175000017500000000147513262465526014771 0ustar dougdougPNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?\IcQ32D 1sq|۫/~bX°>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/treelogo.png0000644000175000017500000000305013262465526015654 0ustar dougdougPNG  IHDR szzsBIT|d pHYs:tEXtSoftwarewww.inkscape.org<IDATXklU3ݲݶnKˣح -c|R1!Q P!LbD >01/Pk0Q1ՂFڅ>ѝ\?,fwfM<=ϜsgF"Ο65`>P 4^b=RT_@,3߆oqU¹ZLrSk<7>xm|%?X"n6ʟ`䧆U!S" t!F{x\9}%(.{%kƝ?(6SCiü~ÿP0;6Snfq2=ϯ{9ND pu*-5Q$_@<H%{:l@Q#_úE̛n:_Wrv(3xRNN|50rwO4s1ҕ6֜|6-J^;@<4L$Z 2˒,BZy2#9Ry;oYH9b `fQ#ۉ;={XZ6#y *^JaO1Ql/C.+؂}9>;+7e_ٮJ@Yw N jmQ @& S&d3 q!n PXe,R+ J ?&±Ϸis xۍ8J\tsoiu5UUҘKWxJi&"7}VG!7M'24@dd Q1'X)CJ_u2}uqzL^a1Wʘd7ђ2 PbQJ˯կZ&S7yUD p WUUIczb=w:IENDB`TreeLine/icons/tree/trash.png0000644000175000017500000000141213262465526015155 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/check_2.png0000644000175000017500000000122513262465526015334 0ustar dougdougPNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<'IDATxb?% (8Qc@7 @2@ĬHl(A!A X2lj okȧdP}گ? ?YB/oV qj $dIH6]'P"@10" H3]"rTW__WہÀꞀOS ; B׀&03$0Z11|88> 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/task_2.png0000644000175000017500000000106013262465526015216 0ustar dougdougPNG  IHDRa>tEXtCommentCreated 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/folder_2.png0000644000175000017500000000070013262465526015527 0ustar dougdougPNG  IHDRabKGDCuIDATx͓K/QN*Jذ]XBDbo#7d%.fhEhFU:sXqbKy |1=ℕG9vN)>vWQ*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/euro.png0000644000175000017500000000051613262465526015012 0ustar dougdougPNG  IHDRagAMA abKGD pHYs  ~tIME  8 IDATxڭ=@slhmH&SPr/`mmhhXX"%7Xp EUUueL[&p1B-MYȩLή-pNc7VsGHҜ8ω"o1d` }/Xy{,M뺿T[P=i01"Is $oz'm>IENDB`TreeLine/icons/tree/round_minus.png0000644000175000017500000000575413262465526016413 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'IENDB`TreeLine/icons/tree/person.png0000644000175000017500000000143413262465526015346 0ustar dougdougPNG  IHDRabKGD pHYs  ~tIME *tEXtCommentCreated with The GIMPd%nIDATxڥMl qvwE?v)D6u@$n.8".ĉ~4muIwg;'=ɛ kx L?Yp%nꬨHr]~wWG"0J_^ Ţ}Pwwum7, @j汵 9Qex#J XFIh5j=y] ,|Ȭ(YPڸ"b *$"MχtFI t.Jt\: ,YD6gvD'ɣvFc}TR%̣frl7PE(wnt|IKF%vuse+kL7rms˾ވ_ HԱduMU*=3ywx_EUMݹun_0F(kv6p qy5%9TE]0S\S#BS*:DuޮdW|?y|x2L)Ba2cc13YXajyġ6/UUBPXMO%3=& gO^ m CG~N؀(GIENDB`TreeLine/icons/tree/tux_2.png0000644000175000017500000000111213262465526015072 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/wrench.png0000644000175000017500000000203713262465526015326 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/tree/round_plus.png0000644000175000017500000000605313262465526016234 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/bullet_1.png0000644000175000017500000000117213262465526015546 0ustar dougdougPNG  IHDRabKGDC pHYs  tIMEjktEXtCommentCreated with The GIMPd%nIDATx?nAH,9E#g(@p 9Gj'c?ޝG)XIi>o40j}\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/arrow_3.png0000644000175000017500000000045213262465526015413 0ustar dougdougPNG  IHDRaIDATx͓=A_w̌ zNm$}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/book_2.png0000644000175000017500000000121113262465526015204 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]] -~KKKzju.Q۶1Fu!J㜶,ijjjϟ?]|9[׿jWGGGO0hEizLFs ($ˍ9Mj͍---JrR! XT>p8J)bG)j_?~AP(\pn)%0R qc|뤥IENDB`TreeLine/icons/tree/book_3.png0000644000175000017500000000116613262465526015216 0ustar dougdougPNG  IHDRabKGD+IDATxڵOHTQOFơD%ҢMkPв "I\ЦR$ 6.R' Drƍ.4q{}8HP:pv"ki@(`gbIr_seQTe[e<]v q >~+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ϣ`,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/smiley_2.png0000644000175000017500000000145613262465526015567 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/lock_2.png0000644000175000017500000000126513262465526015213 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/default.png0000644000175000017500000000124113262465526015460 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@BfPU1_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ρ'^&HQmIENDB`TreeLine/templates/0000755000175000017500000000000013262465526013254 5ustar dougdougTreeLine/templates/110en_Long_Text.trln0000644000175000017500000000210413262465526016761 0ustar dougdoug{ "formats": [ { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Text", "fieldtype": "Text", "lines": 8 } ], "formatname": "LONG_TEXT", "outputlines": [ "{*Name*}", "{*Text*}" ], "titleline": "{*Name*}" } ], "nodes": [ { "children": [ "d8ef9244959111e7a8357054d2175f18" ], "data": { "Name": "Parent", "Text": "Parent text" }, "format": "LONG_TEXT", "uid": "d8ef8eb6959111e7a8357054d2175f18" }, { "children": [], "data": { "Name": "Child", "Text": "Child text" }, "format": "LONG_TEXT", "uid": "d8ef9244959111e7a8357054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "d8ef8eb6959111e7a8357054d2175f18" ] } }TreeLine/templates/210en_Contact_List.trln0000644000175000017500000000736013262465526017456 0ustar dougdoug{ "formats": [ { "childtype": "PERSON", "fields": [ { "fieldname": "Type", "fieldtype": "Text" } ], "formatname": "CATEGORY", "outputlines": [ "{*Type*}" ], "titleline": "{*Type*}" }, { "fields": [ { "fieldname": "FirstName", "fieldtype": "Text", "sortkeynum": 2 }, { "fieldname": "LastName", "fieldtype": "Text", "sortkeynum": 1 }, { "fieldname": "Street", "fieldtype": "Text" }, { "fieldname": "City", "fieldtype": "Text" }, { "fieldname": "State", "fieldtype": "Text" }, { "fieldname": "Zip", "fieldtype": "Text" }, { "fieldname": "HomePhone", "fieldtype": "Text" }, { "fieldname": "WorkPhone", "fieldtype": "Text" }, { "fieldname": "MobilePhone", "fieldtype": "Text" }, { "fieldname": "Birthday", "fieldtype": "Date", "format": "%B %-d, %Y" }, { "fieldname": "Email", "fieldtype": "Text" } ], "formatname": "PERSON", "outputlines": [ "{*FirstName*} {*LastName*}", "{*Street*}", "{*City*}, {*State*} {*Zip*}", "{*HomePhone*} (H)", "{*WorkPhone*} (W)", "{*MobilePhone*} (M)", "DoB: {*Birthday*}", "{*Email*}" ], "titleline": "{*FirstName*} {*LastName*}" } ], "nodes": [ { "children": [ "f402b796959111e7a8357054d2175f18", "f402c33a959111e7a8357054d2175f18" ], "data": { "Type": "Main" }, "format": "CATEGORY", "uid": "f402b5ac959111e7a8357054d2175f18" }, { "children": [ "f402be26959111e7a8357054d2175f18" ], "data": { "Type": "Friends" }, "format": "CATEGORY", "uid": "f402b796959111e7a8357054d2175f18" }, { "children": [], "data": { "City": "Atlantis", "Email": "john.doe@nowhere.com", "FirstName": "John", "HomePhone": "(123) 555-4567", "LastName": "Doe", "State": "CA", "Street": "1492 Columbus Lane", "WorkPhone": "(123) 555-9876", "Zip": "98765" }, "format": "PERSON", "uid": "f402be26959111e7a8357054d2175f18" }, { "children": [ "f402c448959111e7a8357054d2175f18" ], "data": { "Type": "Family" }, "format": "CATEGORY", "uid": "f402c33a959111e7a8357054d2175f18" }, { "children": [], "data": { "Birthday": "1955-02-08", "City": "Britania", "FirstName": "Jane", "HomePhone": "(123) 490-4909", "LastName": "Roe", "State": "NM", "Street": "1812 War Lane", "Zip": "87560" }, "format": "PERSON", "uid": "f402c448959111e7a8357054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "f402b5ac959111e7a8357054d2175f18" ] } }TreeLine/templates/220en_Book_List.trln0000644000175000017500000000647313262465526016762 0ustar dougdoug{ "formats": [ { "childtype": "BOOK", "fields": [ { "fieldname": "AuthorFirstName", "fieldtype": "Text", "sortkeynum": 2 }, { "fieldname": "AuthorLastName", "fieldtype": "Text", "sortkeynum": 1 } ], "formatname": "AUTHOR", "icon": "book_3", "outputlines": [ "{*AuthorFirstName*} {*AuthorLastName*}" ], "titleline": "{*AuthorFirstName*} {*AuthorLastName*}" }, { "fields": [ { "fieldname": "Title", "fieldtype": "Text", "sortkeynum": 2 }, { "fieldname": "Copyright", "fieldtype": "Number", "format": "0000", "sortkeynum": 1 }, { "fieldname": "ReadDate", "fieldtype": "Date", "format": "%B, %Y" }, { "fieldname": "Plot", "fieldtype": "Text" } ], "formatname": "BOOK", "icon": "book_1", "outputlines": [ "\"{*Title*}\"", "(c) {*Copyright*}", "Last Read: {*ReadDate*}", "{*Plot*}" ], "titleline": "\"{*Title*}\"" }, { "childtype": "AUTHOR", "fields": [ { "fieldname": "NAME", "fieldtype": "Text" } ], "formatname": "CATEGORY", "outputlines": [ "{*NAME*}" ], "titleline": "{*NAME*}" } ], "nodes": [ { "children": [ "0b7bffe0959211e7a8357054d2175f18", "0b7c0850959211e7a8357054d2175f18" ], "data": { "NAME": "SF Books" }, "format": "CATEGORY", "uid": "0b7bfb76959211e7a8357054d2175f18" }, { "children": [ "0b7c0530959211e7a8357054d2175f18" ], "data": { "AuthorFirstName": "Orson Scott", "AuthorLastName": "Card" }, "format": "AUTHOR", "uid": "0b7bffe0959211e7a8357054d2175f18" }, { "children": [], "data": { "Copyright": "1985", "Plot": "Young boy is taught fighting and leadership", "ReadDate": "2007-04-30", "Title": "Ender's Game" }, "format": "BOOK", "uid": "0b7c0530959211e7a8357054d2175f18" }, { "children": [ "0b7c0968959211e7a8357054d2175f18" ], "data": { "AuthorFirstName": "Isaac", "AuthorLastName": "Asimov" }, "format": "AUTHOR", "uid": "0b7c0850959211e7a8357054d2175f18" }, { "children": [], "data": { "Copyright": "1951", "Plot": "Psychohistory predicts the fall of empire", "Title": "Foundation" }, "format": "BOOK", "uid": "0b7c0968959211e7a8357054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "0b7bfb76959211e7a8357054d2175f18" ] } }TreeLine/templates/exports/0000755000175000017500000000000013262465526014760 5ustar dougdougTreeLine/templates/exports/live_tree_export.js0000644000175000017500000011534413262465526020705 0ustar dougdoug// live_tree_export.js, provides javascript code for a read-only tree view // Works with TreeLine, an information storage program // Copyright (C) 2018, 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. "use strict"; var spotDict = {}; var rootSpots = []; var treeFormats = {}; var selectedSpot = null; var openMarker = "\u2296"; var closedMarker = "\u2295"; var leafMarker = "\u25CB"; function main() { if (dataFileName) { if (dataFilePath) { dataFileName = dataFilePath + "/" + dataFileName; } loadFile(dataFileName); } else { loadData(document.getElementById("json").innerHTML); } } function loadFile(filePath) { // initial load from file link var xhttp = new XMLHttpRequest(); xhttp.overrideMimeType("application/json"); xhttp.open("GET", filePath, true); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { loadData(this.responseText); } } xhttp.send(null); } function loadData(textData) { // initial load from file data var fileData = JSON.parse(textData); fileData.formats.forEach(function(formatData) { var formatName = formatData.formatname; treeFormats[formatName] = new NodeFormat(formatData); }); var node, spot; fileData.nodes.forEach(function(nodeData) { node = new TreeNode(treeFormats[nodeData.format], nodeData); spot = new TreeSpot(node, nodeData.uid); }); rootSpots = fileData.properties.topnodes.map(function(id) { return spotDict[id]; }); rootSpots.forEach(function(rootSpot) { rootSpot.nodeRef.assignRefs(null); }); var rootElement = document.getElementById("rootlist"); rootSpots.forEach(function(rootSpot) { if (rootSpot.nodeRef.childList.length > 0) rootSpot.open = true; rootSpot.outputElement(rootElement); }); } function TreeSpot(nodeRef, uId) { // class to store node positions (unique even for cloned nodes) this.nodeRef = nodeRef; this.uId = uId; this.parentSpot; this.open = false; spotDict[uId] = this; nodeRef.spotRefs.push(this); } TreeSpot.prototype.childSpots = function() { // return an array of child spots return this.nodeRef.childList.map(function(node) { return node.matchedSpot(this); }, this); } TreeSpot.prototype.outputElement = function(parentElement) { // recursively output html tree elements var node = this.nodeRef; var element = document.createElement("li"); var markerSpan = document.createElement("span"); var markerText = leafMarker; if (node.childList.length > 0) { markerText = this.open ? openMarker : closedMarker; } markerSpan.appendChild(document.createTextNode(markerText)); markerSpan.className = "marker"; element.appendChild(markerSpan); var textSpan = document.createElement("span"); textSpan.appendChild(document.createTextNode(node.formatRef. formatTitle(this))); textSpan.className = "nodetext"; element.appendChild(textSpan); element.setAttribute("id", this.uId); parentElement.appendChild(element); if (this.open && node.childList.length > 0) this.openChildren(element); } TreeSpot.prototype.openChildren = function(parentElement) { // output children of this node var listElement = document.createElement("ul"); parentElement.appendChild(listElement); this.childSpots().forEach(function(childSpot) { childSpot.outputElement(listElement); }); } TreeSpot.prototype.toggleOpen = function() { // toggle this spot's opened/closed state if (this.nodeRef.childList.length == 0) return; this.open = !this.open; var element = document.getElementById(this.uId); if (this.open) { element.childNodes[0].innerHTML = openMarker; this.openChildren(element); } else { element.childNodes[0].innerHTML = closedMarker; var elementList = element.childNodes; for (var i = 0; i < elementList.length; i++) { if (elementList[i].tagName == "UL") { element.removeChild(elementList[i]); } } } } TreeSpot.prototype.openParents = function() { // open all parent spots of this spot var ancestors = []; var spot = this.parentSpot; var element; while (spot) { ancestors.unshift(spot); spot = spot.parentSpot; } ancestors.forEach(function(ancestor) { if (!ancestor.open) { ancestor.open = true; element = document.getElementById(ancestor.uId); element.childNodes[0].innerHTML = openMarker; ancestor.openChildren(element); } }); } TreeSpot.prototype.select = function() { // change selection to this var prevSpot = selectedSpot; selectedSpot = this; if (prevSpot) { var prevElem = document.getElementById(prevSpot.uId); if (prevElem) prevElem.childNodes[1].classList.remove("selected"); } var element = document.getElementById(this.uId); element.childNodes[1].classList.add("selected"); var outputGroup = new OutputGroup(); document.getElementById("output").innerHTML = outputGroup.getText(); } TreeSpot.prototype.prevTreeSpot = function() { // return the previous open spot in tree order var pos, node, sibling; if (this.parentSpot) { pos = this.parentSpot.nodeRef.childList.indexOf(this.nodeRef); if (pos <= 0) return this.parentSpot; node = this.parentSpot.nodeRef.childList[pos - 1]; sibling = node.matchedSpot(this.parentSpot); } else { pos = rootSpots.indexOf(this); if (pos <= 0) return null; sibling = rootSpots[pos - 1]; } while (sibling.open) { node = sibling.nodeRef.childList[sibling.nodeRef.childList. length - 1]; sibling = node.matchedSpot(sibling); } return sibling; } TreeSpot.prototype.nextTreeSpot = function() { // return the next open spot in tree order if (this.open) { return this.nodeRef.childList[0].matchedSpot(this); } var pos, sibling; var ancestor = this; while (ancestor.parentSpot) { pos = ancestor.parentSpot.nodeRef.childList.indexOf(ancestor.nodeRef); sibling = ancestor.parentSpot.nodeRef.childList[pos + 1]; if (sibling) { return sibling.matchedSpot(ancestor.parentSpot); } ancestor = ancestor.parentSpot; } pos = rootSpots.indexOf(ancestor); sibling = rootSpots[pos + 1]; if (sibling) return sibling; return null; } function TreeNode(formatRef, fileData) { // class to store nodes this.formatRef = formatRef; this.data = fileData.data; this.tmpChildRefs = fileData.children; this.spotRefs = []; this.childList = []; } TreeNode.prototype.assignRefs = function(parentSpot) { // recursively add actual refs to child nodes and parent spots var spot = this.spotRefs[0]; if (spot.parentSpot !== undefined) { // cloned node var id = spot.uId; var num = 1; do { id = id + "_" + num; num += 1; } while (id in spotDict); spot = new TreeSpot(this, id); } spot.parentSpot = parentSpot; this.childList.forEach(function(child) { // for clones (2nd time thru) child.assignRefs(spot); }); var childNode; this.tmpChildRefs.forEach(function(childId) { // for first time thru childNode = spotDict[childId].nodeRef; this.childList.push(childNode); childNode.assignRefs(spot); }, this); this.tmpChildRefs = []; } TreeNode.prototype.matchedSpot = function(parentSpot) { // return the spot for this node that matches the given parent spot for (var i = 0; i < this.spotRefs.length; i++) { if (this.spotRefs[i].parentSpot === parentSpot) { return this.spotRefs[i]; } } return null; } function NodeFormat(formatData) { // class to store node format data and format output this.fieldDict = {}; formatData.fields.forEach(function(fieldData) { this.fieldDict[fieldData.fieldname] = new FieldFormat(fieldData); }, this); this.spaceBetween = valueOrDefault(formatData, "spacebetween", true); this.formatHtml = valueOrDefault(formatData, "formathtml", false); this.outputSeparator = valueOrDefault(formatData, "outputsep", ", "); this.siblingPrefix = ""; this.siblingSuffix = ""; this.titleLine = this.parseLine(formatData.titleline); var lines = formatData.outputlines; this.useBullets = valueOrDefault(formatData, "bullets", false); if (this.useBullets) { this.siblingPrefix = "

      "; this.siblingSuffix = "
    "; if (lines != [""]) { lines[0] = "
  • " + lines[0]; lines[lines.length - 1] += "
  • "; } } this.useTables = valueOrDefault(formatData, "tables", false); if (this.useTables) { lines = lines.filter(String); var newLines = []; var headings = []; var head, firstPart, parts; lines.forEach(function(line) { head = ""; firstPart = this.parseLine(line)[0]; if (typeof firstPart == "string" && firstPart.indexOf(":") >= 0) { parts = line.split(":"); head = parts.shift(); line = parts.join(""); } newLines.push(line.trim()); headings.push(head.trim()); }, this); this.siblingPrefix = ''; if (headings.filter(String).length > 0) { this.siblingPrefix += ""; headings.forEach(function(hd) { this.siblingPrefix += ""; }, this); this.siblingPrefix += ""; } this.siblingSuffix = "
    " + hd + "
    "; lines = newLines.map(function(line) { return "" + line + ""; }); lines[0] = "" + lines[0]; lines[lines.length - 1] += ""; } this.outputLines = lines.map(this.parseLine, this); } NodeFormat.prototype.parseLine = function(text) { // parse text with embedded fields, return list of fields and text var segments = text.split(/({\*(?:\**|\?|!|&|#)[\w_\-.]+\*})/g); return segments.map(this.parseField, this).filter(String); } NodeFormat.prototype.parseField = function(text) { // parse text field, return field type or plain text if not a field var field; var match = /{\*(\**|\?|!|&|#)([\w_\-.]+)\*}/g.exec(text); if (match) { var modifier = match[1]; var fieldName = match[2]; if (modifier == "" && fieldName in this.fieldDict) { return this.fieldDict[fieldName]; } else if (modifier.match(/^\*+$/)) { field = new FieldFormat({"fieldname": fieldName, "fieldtype": "AncestorLevel"}); field.ancestorLevel = modifier.length; field.placeholder = true; return field; } else if (modifier == "?") { field = new FieldFormat({"fieldname": fieldName, "fieldtype": "AnyAncestor"}); field.placeholder = true; return field; } else if (modifier == "&") { field = new FieldFormat({"fieldname": fieldName, "fieldtype": "ChildList"}); field.placeholder = true; return field; } else if (modifier == "#") { match = /[^0-9]+([0-9]+)$/.exec(fieldName); if (match && match[1] != "0") { field = new FieldFormat({"fieldname": fieldName, "fieldtype": "DescendantCount"}); field.descendantLevel = Number(match[1]); field.placeholder = true; return field; } } else if (modifier == "!") { field = new FieldFormat({"fieldname": fieldName, "fieldtype": "StaticFileInfo"}); if (fieldName == "File_Name") { field.staticInfo = dataFileName; } else if (fieldName == "File_Path") { field.staticInfo = dataFilePath; } field.placeholder = true; return field; } } return text; } NodeFormat.prototype.formatTitle = function(spot) { // return a string with formatted title data var result = this.titleLine.map(function(part) { if (typeof part.outputText === "function") { return part.outputText(spot, true, this.formatHtml); } return part; }, this); return result.join("").trim().split("\n", 1)[0]; } NodeFormat.prototype.formatOutput = function(spot, keepBlanks) { // return a list of formatted text output lines var line, numEmptyFields, numFullFields, text, match; var result = []; this.outputLines.forEach(function(lineData) { line = ""; numEmptyFields = 0; numFullFields = 0; lineData.forEach(function(part) { if (typeof part.outputText === "function") { text = part.outputText(spot, false, this.formatHtml); if (text) { numFullFields += 1; } else { numEmptyFields += 1; } line += text; } else { if (!this.formatHtml) { part = escapeHtml(part); } line += part; } }, this); if (keepBlanks || numFullFields > 0 || numEmptyFields == 0) { result.push(line); } else if (this.formatHtml && result.length > 0) { match = /.*(|)$/gi.exec(line); if (match) { result[result.length - 1] += match[1]; } } }, this); return result; } function FieldFormat(fieldData) { // class to store field format data and format field output this.name = fieldData.fieldname; this.fieldType = fieldData.fieldtype; this.format = valueOrDefault(fieldData, "format", ""); this.prefix = valueOrDefault(fieldData, "prefix", ""); this.suffix = valueOrDefault(fieldData, "suffix", ""); this.mathResultType = valueOrDefault(fieldData, "resulttype", "number"); this.placeholder = false; this.ancestorLevel = 0; this.descendantLevel = 0; this.staticInfo = ""; if (this.fieldType == "Numbering") { this.numberingFormats = initNumbering(this.format); } if (this.fieldType == "Choice" || this.fieldType == "Combination") { var formatText = this.format.replace(/\/\//g, "\0"); if (valueOrDefault(fieldData, "evalhtml", false)) { formatText = escapeHtml(formatText); } this.choiceList = formatText.split("/").map(function(text) { return text.replace(/\0/g, "/"); }); } } FieldFormat.prototype.outputText = function(spot, titleMode, formatHtml) { // return formatted output text for this field in this node var splitValue, outputSep, selections, result, match, i, field; var newNodes, prevNodes; var value = valueOrDefault(spot.nodeRef.data, this.name, ""); if (!value && !this.placeholder) return ""; switch (this.fieldType) { case "OneLineText": value = value.split("
    ", 1)[0]; break; case "SpacedText": value = "
    " + value + "
    "; break; case "Number": var num = Number(value); value = formatNumber(num, this.format); break; case "Math": if (this.mathResultType == "number") { var num = Number(value); value = formatNumber(num, this.format); } else if (this.mathResultType == "date") { value = formatDate(value, this.format); } else if (this.mathResultType == "time") { value = formatTime(value, this.format); } else if (this.mathResultType == "boolean") { value = formatBoolean(value, this.format); } break; case "Numbering": value = formatNumbering(value, this.numberingFormats); break; case "Date": value = formatDate(value, this.format); break; case "Time": value = formatTime(value, this.format); break; case "DateTime": splitValue = value.split(" "); value = formatDate(splitValue[0], this.format); value = formatTime(splitValue[1], value); break; case "Choice": if (this.choiceList.indexOf(value) < 0) value = "#####"; break; case "Combination": outputSep = spot.nodeRef.formatRef.outputSeparator; value = value.replace(/\/\//g, "\0"); selections = value.split("/").map(function(text) { return text.replace(/\0/g, "/"); }); result = this.choiceList.filter(function(text) { return selections.indexOf(text) >= 0; }); if (result.length == selections.length) { value = result.join(outputSep); } else { value = "#####"; } break; case "AutoCombination": outputSep = spot.nodeRef.formatRef.outputSeparator; value = value.replace(/\/\//g, "\0"); selections = value.split("/").map(function(text) { return text.replace(/\0/g, "/"); }); value = selections.join(outputSep); break; case "Boolean": value = formatBoolean(value, this.format); break; case "ExternalLink": case "InternalLink": if (titleMode) { match = /]*href="([^"]+)"[^>]*>([\S\s]*?)<\/a>/i. exec(value); if (match) { value = match[2].trim(); if (!value) { value = match[1]; if (value.startsWith("#")) value = value.substr(1); } } } break; case "Picture": if (titleMode) { match = /]*src="([^"]+)"[^>]*>/i.exec(value); if (match) value = match(1).trim(); } break; case "RegularExpression": match = new RegExp(this.format).exec(unescapeHtml(value)); if (!match || match[0] != unescapeHtml(value)) { value = "#####"; } break; case "AncestorLevel": value = ""; for (i = 0; i < this.ancestorLevel; i++) { spot = spot.parentSpot; } if (spot) { field = spot.nodeRef.formatRef.fieldDict[this.name]; if (field) { value = field.outputText(spot, titleMode, formatHtml); } } break; case "AnyAncestor": value = ""; while (spot.parentSpot) { spot = spot.parentSpot; field = spot.nodeRef.formatRef.fieldDict[this.name]; if (field) { value = field.outputText(spot, titleMode, formatHtml); break; } } break; case "ChildList": result = []; spot.childSpots().forEach(function(childSpot) { field = childSpot.nodeRef.formatRef.fieldDict[this.name]; if (field) { result.push(field.outputText(childSpot, titleMode, formatHtml)); } }, this); outputSep = spot.nodeRef.formatRef.outputSeparator; value = result.join(outputSep); break; case "DescendantCount": newNodes = [childSpot.nodeRef]; for (i = 0; i < this.descendantLevel; i++) { prevNodes = newNodes; newNodes = []; prevNodes.forEach(function(child) { newNodes = newNodes.concat(child.childList); }); } value = newNodes.length.toString(); break; case "StaticFileInfo": value = this.staticInfo; break; } var prefix = this.prefix; var suffix = this.suffix; if (titleMode) { value = removeMarkup(value); if (formatHtml) { prefix = removeMarkup(prefix); suffix = removeMarkup(suffix); } } else if (!formatHtml) { prefix = escapeHtml(prefix); suffix = escapeHtml(suffix); } return prefix + value + suffix; } function OutputItem(spot, level) { // class to store output for a single node var format = spot.nodeRef.formatRef; if (format.useTables) { this.textLines = format.formatOutput(spot, true); } else { this.textLines = format.formatOutput(spot, false). map(function(line) { return line + "
    "; }); } this.level = level; this.uId = spot.uId; this.addSpace = format.spaceBetween; this.siblingPrefix = format.siblingPrefix; this.siblingSuffix = format.siblingSuffix; if (format.useBullets && this.textLines.length > 0) { this.textLines[this.textLines.length - 1] = this.textLines[this.textLines.length - 1].slice(0, -6); } } OutputItem.prototype.addIndent = function(prevLevel, nextLevel) { // add
    tags to define indent levels in the output var i; for (i = 0; i < this.level - prevLevel; i++) { this.textLines[0] = "
    " + this.textLines[0]; } for (i = 0; i < this.level - nextLevel; i++) { this.textLines[this.textLines.length - 1] += "
    "; } } OutputItem.prototype.addSiblingPrefix = function() { // add the sibling prefix before this output if (this.siblingPrefix) { this.textLines[0] = this.siblingPrefix + this.textLines[0]; } } OutputItem.prototype.addSiblingSuffix = function() { // add the sibling suffix after this output if (this.siblingSuffix) { this.textLines[this.textLines.length - 1] += this.siblingSuffix; } } OutputItem.prototype.equalPrefix = function(otherItem) { // return true if sibling prefixes and suffixes are equal return (this.siblingPrefix == otherItem.siblingPrefix && this.siblingSuffix == otherItem.siblingSuffix); } function OutputGroup() { // class to store and modify output lines this.itemList = []; if (selectedSpot) { this.itemList.push(new OutputItem(selectedSpot, 0)); this.addChildren(selectedSpot, 0); if (this.hasPrefixes()) this.combineAllSiblings(); this.addBlanksBetween(); this.addIndents(); } } OutputGroup.prototype.addChildren = function(spot, level) { // recursively add output items for descendants spot.childSpots().forEach(function(childSpot) { this.itemList.push(new OutputItem(childSpot, level + 1)); this.addChildren(childSpot, level + 1); }, this); } OutputGroup.prototype.addBlanksBetween = function() { // add blank lines between items based on node format for (var i = 0; i < this.itemList.length - 1; i++) { if (this.itemList[i].addSpace || this.itemList[i + 1].addSpace) { var lines = this.itemList[i].textLines; lines[lines.length - 1] += "
    " } } } OutputGroup.prototype.addIndents = function() { // add nested
    elements to define indentations in the output var prevLevel = 0; var nextLevel; for (var i = 0; i < this.itemList.length; i++) { if (i + 1 < this.itemList.length) { nextLevel = this.itemList[i + 1].level; } else { nextLevel = 0; } this.itemList[i].addIndent(prevLevel, nextLevel); prevLevel = this.itemList[i].level; } } OutputGroup.prototype.hasPrefixes = function() { // return true if sibling prefixes or suffixes are found var items = this.itemList.filter(function(item) { return item.siblingPrefix || item.siblingSuffix; }); return items.length > 0; } OutputGroup.prototype.combineAllSiblings = function() { // group all sibling items with the same prefix into single items // also add sibling prefixes and suffixes and spaces in between var newItems = []; var prevItem = null; this.itemList.forEach(function(item) { if (prevItem) { if (item.level == prevItem.level && item.equalPrefix(prevItem)) { if (item.addSpace || prevItem.addSpace) { prevItem.textLines[prevItem.textLines.length - 1] += "
    "; } prevItem.textLines = prevItem.textLines.concat(item.textLines); } else { prevItem.addSiblingSuffix(); newItems.push(prevItem); item.addSiblingPrefix(); prevItem = item; } } else { item.addSiblingPrefix(); prevItem = item; } }); prevItem.addSiblingSuffix(); newItems.push(prevItem); this.itemList = newItems; } OutputGroup.prototype.getText = function() { // return a text string for all output if (this.itemList.length == 0) return ""; var lines = []; this.itemList.forEach(function(item) { lines = lines.concat(item.textLines); }); return lines.join("\n"); } window.onclick = function(event) { // handle mouse clicks for open/close and selection var spot; if (event.target.tagName == "SPAN") { var elemId = event.target.parentElement.getAttribute("id"); spot = spotDict[elemId]; if (spot) { if (event.target.classList.contains("marker")) { spot.toggleOpen(); } else if (event.target.classList.contains("nodetext")) { spot.select(); } } } else if (event.target.tagName == "A") { var addr = event.target.getAttribute("href"); if (addr.startsWith("#")) { event.preventDefault(); spot = spotDict[addr.substr(1)]; if (spot) { spot.openParents(); spot.select(); } } } } window.onkeydown = function(event) { // handle arrow keys for selection management var spot; switch (event.which) { case 38: // up arrow if (selectedSpot) { spot = selectedSpot.prevTreeSpot(); } else { spot = rootSpots[0]; } if (spot) spot.select(); event.preventDefault(); break; case 40: // down arrow if (selectedSpot) { spot = selectedSpot.nextTreeSpot(); } else { spot = rootSpots[0]; } if (spot) spot.select(); event.preventDefault(); break; case 37: // left arrow if (selectedSpot && selectedSpot.open) selectedSpot.toggleOpen(); event.preventDefault(); break; case 39: // right arrow if (selectedSpot && !selectedSpot.open) selectedSpot.toggleOpen(); event.preventDefault(); break; } } function valueOrDefault(object, name, dflt) { // return the value of the named property or the default value var value = object[name]; if (value !== undefined) return value; return dflt; } function escapeHtml(text) { // return the given string with &, <, > escaped return text.replace(/&/g, '&').replace(//g, '>'); } function unescapeHtml(text) { // return the given string with &, <, > unescaped return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); } function removeMarkup(text) { // return text with all HTML Markup removed and entities unescaped return text.replace(/<.*?>/g, "").replace(/&/g, "&"). replace(/</g, "<").replace(/>/g, ">"); } function formatNumber(num, format) { // return a formttted string for the given number var formatParts = format.split(/e/i); if (formatParts.length < 2) return formatBasicNumber(num, format); var formatMain = formatParts[0]; var formatExp = formatParts[1]; var exp = Math.floor(Math.log(Math.abs(num)) / Math.LN10); num = num / Math.pow(10, exp); var totalPlcs = (formatMain.match(/[#0]/g) || []).length; if (totalPlcs < 1) totalPlcs = 1; num = Number(num.toFixed(totalPlcs - 1)); var radix = "."; if (format.indexOf("\\,") < 0 && (format.indexOf("\\.") >= 0 || (format.indexOf(",") >= 0 && format.indexOf(".") < 0))) { radix = ","; } var formatWhole = formatMain.split(radix)[0]; var wholePlcs = (formatWhole.match(/[#0]/g) || []).length; var expChg = wholePlcs - Math.floor(Math.log(Math.abs(num)) / Math.LN10) - 1; num = num * Math.pow(10, expChg); exp -= expChg; var c = format.indexOf("e") >= 0 ? "e" : "E"; return formatBasicNumber(num, formatMain) + c + formatBasicNumber(exp, formatExp) } function formatBasicNumber(num, format) { // return a formatted string for the given number without an exponent var radix; if (format.indexOf("\\,") < 0 && (format.indexOf("\\.") >= 0 || (format.indexOf(",") >= 0 && format.indexOf(".") < 0))) { radix = ","; format.replace(/\\./g, "."); } else { radix = "."; format.replace(/\\,/g, ","); } var formatParts = format.split(radix); var formatWhole = formatParts[0].split(""); var formatFract = formatParts.length > 1 ? formatParts[1] : ""; var decPlcs = (formatFract.match(/[#0]/g) || []).length; formatFract = formatFract.split(""); var numParts = num.toFixed(decPlcs).split("."); var numWhole = numParts[0].split(""); var numFract = numParts.length > 1 ? numParts[1] : ""; numFract = numFract.replace(/0+$/g, "").split(""); var sign = "+"; if (numWhole[0] == "-") sign = numWhole.shift(); var c; var result = []; while (numWhole.length || formatWhole.length) { c = formatWhole.length ? formatWhole.pop() : ""; if (c && "#0 +-".indexOf(c) < 0) { if (numWhole.length || formatWhole.indexOf("0") >= 0) { result.unshift(c); } } else if (numWhole.length && c != " ") { result.unshift(numWhole.pop()); if (c && "+-".indexOf(c) >= 0) { formatWhole.push(c); } } else if ("0 ".indexOf(c) >= 0) { result.unshift(c); } else if ("+-".indexOf(c) >= 0) { if (sign == "-" || c == "+") { result.unshift(sign); } sign = ""; } } if (sign == "-") { if (result[0] == " ") { result = [result.join("").replace(/\s(?!\s)/, "-")]; } else { result.unshift("-"); } } if (formatFract.length || (format.length && format.charAt(format.length - 1) == radix)) { result.push(radix); } while (formatFract.length) { c = formatFract.shift(); if ("#0 ".indexOf(c) < 0) { if (numFract.length || formatFract.indexOf("0") >= 0) { result.push(c); } } else if (numFract.length) { result.push(numFract.shift()); } else if ("0 ".indexOf(c) >= 0) { result.push("0"); } } return result.join(""); } function initNumbering(format) { // return an array of basic numbering formats var sectionStyle = false; var tmpFormat = format.replace(/\.\./g, ".").replace(/\/\//g, "\0"); var delim = "/"; var formats = tmpFormat.split(delim); if (formats.length < 2) { tmpFormat = format.replace(/\/\//g, "/").replace(/\.\./g, "\0"); delim = "."; formats = tmpFormat.split(delim); if (formats.length > 1) sectionStyle = true; } formats = formats.map(function(text) { return new NumberingFormat(text.replace(/\0/g, delim), sectionStyle); }); return formats; } function NumberingFormat(formatStr, sectionStyle) { // class to store basic formatting for an element of numbering fields this.romanDict = {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"}; this.sectionStyle = sectionStyle; var match = /(.*)([1AaIi])(.*)/.exec(formatStr); if (match) { this.prefix = match[1]; this.format = match[2]; this.suffix = match[3]; } else { this.prefix = formatStr; this.format = "1"; this.suffix = ""; } } NumberingFormat.prototype.numString = function(num) { var result = ""; var digit; var factor = 1000; if (num > 0) { if (this.format == "1") { result = num.toString(); } else if (this.format == "A" || this.format == "a") { while (num) { digit = (num - 1) % 26; result = String.fromCharCode(digit + "A".charCodeAt(0)) + result; num = Math.floor((num - digit - 1) / 26); } if (this.format == "a") result = result.toLowerCase(); } else if (num < 4000) { while (num) { digit = num - (num % factor); result += this.romanDict[digit]; factor = Math.floor(factor / 10); num -= digit; } if (this.format == "i") result = result.toLowerCase(); } } return this.prefix + result + this.suffix; } function formatNumbering(value, numFormats) { // return a formatted string for a numbering field var inputNums = value.split(".").map(function(num) { return Number(num); }); if (numFormats[0].sectionStyle) { numFormats = numFormats.slice(); while (numFormats.length < inputNums.length) { numFormats.push(numFormats[numFormats.length - 1]); } var results = inputNums.map(function(num, i) { return numFormats[i].numString(num); }); return results.join("."); } else { var numFormat = numFormats[inputNums.length - 1]; if (!numFormat) numFormat = numFormats[numFormats.length - 1]; return numFormat.numString(inputNums[inputNums.length - 1]); } } function formatDate(storedText, format) { // return a formatted date string var monthNames = ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; var dateArray = storedText.split("-"); var year = dateArray[0]; var month = dateArray[1]; var day = dateArray[2]; var yearNum = Number(year); var monthNum = Number(month); var dayNum = Number(day); format = format.replace(/%-d/g, dayNum).replace(/%d/g, day); format = format.replace(/%a/g, weekday(yearNum, monthNum, dayNum).substr(0, 3)); format = format.replace(/%A/g, weekday(yearNum, monthNum, dayNum)); format = format.replace(/%-m/g, monthNum).replace(/%m/g, month); format = format.replace(/%b/g, monthNames[monthNum].substr(0, 3)); format = format.replace(/%B/g, monthNames[monthNum]); format = format.replace(/%y/g, year.slice(-2)).replace(/%Y/g, year); format = format.replace(/%-U/g, weekNumber(yearNum, monthNum, dayNum)); format = format.replace(/%-j/g, dayOfYear(yearNum, monthNum, dayNum)); return format; } function dayOfYear(year, month, day) { // return the day of year (1 to 366) var daysInMonths = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; var day = daysInMonths[month - 1] + day; if (month > 2 && year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) { day += 1; } return day; } function firstWeekday(year) { // return a number for the weekday of Jan. 1st (0=Sun., 6=Sat.) var y = year - 1; return (y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) + 1) % 7; } function weekday(year, month, day) { // return the weekday name for the given date var weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; var day = (firstWeekday(year) + dayOfYear(year, month, day) - 1) % 7; return weekdays[day]; } function weekNumber(year, month, day) { // return the week number for the given date return Math.floor((dayOfYear(year, month, day) + firstWeekday(year) - 1) / 7); } function formatTime(storedText, format) { // return a formatted time string var timeArray = storedText.split(":"); var hour = timeArray[0]; var minute = timeArray[1]; var second = timeArray[2].split(".")[0]; var microSecond = timeArray[2].split(".")[1]; var hourNum = Number(hour); var minuteNum = Number(minute); var secondNum = Number(second); format = format.replace(/%-H/g, hourNum).replace(/%H/g, hour); var ampm = "AM"; if (hourNum == 0) { hourNum = 12; hour = "12"; } else if (hourNum > 11) { ampm = "PM"; if (hourNum > 12) { hourNum -= 12; hour = hourNum.toString(); if (hourNum < 10) hour = "0" + hour; } } format = format.replace(/%-I/g, hourNum).replace(/%I/g, hour); format = format.replace(/%-M/g, minuteNum).replace(/%M/g, minute); format = format.replace(/%-S/g, secondNum).replace(/%S/g, second); format = format.replace(/%f/g, microSecond).replace(/%p/g, ampm); return format; } function formatBoolean(storedText, format) { // return a formatted boolean string var boolDict = {"true": 0, "false": 1, "t": 0, "f": 1, "yes": 0, "no": 1, "y": 0, "n": 1}; var valueNum = boolDict[storedText.toLowerCase()]; var value = format.split("/")[valueNum]; if (value == undefined) value = "#####"; return value; } main(); TreeLine/templates/exports/live_tree_export.html0000644000175000017500000000104413262465526021224 0ustar dougdoug TreeLine Export
      TreeLine/templates/exports/live_tree_export.css0000644000175000017500000000112713262465526021052 0ustar dougdoug/* live_tree_export.css, provides css for a read-only TreeLine view */ body { background-color: white; color: black; } #tree { width: 40%; height: 100%; top: 0; left: 0; position: fixed; overflow: scroll; } #tree ul { cursor: default; list-style-type: none; } #tree li { text-indent: -1.3em; } .marker { font-size: 0.8em; display: inline-block; width: 1.3em; text-align: center; vertical-align: middle; color: blue; } .selected { color: red; } #output { margin-left: 41%; } #output div { padding-left: 2em; } TreeLine/templates/230en_ToDo_List.trln0000644000175000017500000000771113262465526016732 0ustar dougdoug{ "formats": [ { "childtype": "TASK_UNDONE", "fields": [ { "fieldname": "Name", "fieldtype": "Text" } ], "formathtml": true, "formatname": "CATEGORY", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "condition": "Done == \"true\"", "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Done", "fieldtype": "Boolean", "format": "yes/no", "init": "false" }, { "fieldname": "Urgent", "fieldtype": "Boolean", "format": "yes/no", "init": "false" } ], "formathtml": true, "formatname": "TASK_DONE", "icon": "smiley_4", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Done", "fieldtype": "Boolean", "format": "yes/no", "init": "false" }, { "fieldname": "Urgent", "fieldtype": "Boolean", "format": "yes/no", "init": "false" } ], "formathtml": true, "formatname": "TASK_UNDONE", "generic": "TASK_DONE", "icon": "smiley_2", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}" }, { "condition": "Done == \"false\" and Urgent == \"true\"", "fields": [ { "fieldname": "Name", "fieldtype": "Text" }, { "fieldname": "Done", "fieldtype": "Boolean", "format": "yes/no", "init": "false" }, { "fieldname": "Urgent", "fieldtype": "Boolean", "format": "yes/no", "init": "true" } ], "formathtml": true, "formatname": "TASK_UNDONE_URGENT", "generic": "TASK_DONE", "icon": "smiley_5", "outputlines": [ "{*Name*}" ], "titleline": "{*Name*}!!!" } ], "nodes": [ { "children": [ "1d1b7e56959211e7a8357054d2175f18", "1d1b857c959211e7a8357054d2175f18" ], "data": { "Name": "Conditional Task List" }, "format": "CATEGORY", "uid": "1d1b7c9e959211e7a8357054d2175f18" }, { "children": [ "1d1b81b2959211e7a8357054d2175f18" ], "data": { "Name": "Home Tasks" }, "format": "CATEGORY", "uid": "1d1b7e56959211e7a8357054d2175f18" }, { "children": [], "data": { "Done": "false", "Name": "Mow lawn", "Urgent": "false" }, "format": "TASK_UNDONE", "uid": "1d1b81b2959211e7a8357054d2175f18" }, { "children": [ "1d1b868a959211e7a8357054d2175f18" ], "data": { "Name": "Work Tasks" }, "format": "CATEGORY", "uid": "1d1b857c959211e7a8357054d2175f18" }, { "children": [], "data": { "Done": "false", "Name": "Write documents", "Urgent": "false" }, "format": "TASK_UNDONE", "uid": "1d1b868a959211e7a8357054d2175f18" } ], "properties": { "tlversion": "2.9.0", "topnodes": [ "1d1b7c9e959211e7a8357054d2175f18" ] } }