├── .appveyor.yml ├── .codecov.yml ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── changelog.rst ├── conf.py ├── documentation.rst ├── example.rst ├── genindex.rst ├── index.rst └── installation.rst ├── setup.py ├── tests ├── __init__.py ├── base_widget_testcase.py ├── test_autoscrollbar.py ├── test_filebrowser.py ├── test_pathbutton.py ├── test_recentfiles.py └── test_tooltip.py └── tkfilebrowser ├── __init__.py ├── __main__.py ├── autoscrollbar.py ├── constants.py ├── filebrowser.py ├── functions.py ├── images ├── desktop.png ├── drive.png ├── file.png ├── file_link.png ├── folder.png ├── folder_link.png ├── home.png ├── link_broken.png ├── new_folder.png ├── recent.png └── recent_24.png ├── path_button.py ├── recent_files.py └── tooltip.py /.appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - PYTHON: "C:\\PYTHON27" 4 | - PYTHON: "C:\\PYTHON35" 5 | - PYTHON: "C:\\PYTHON36" 6 | install: 7 | - "%PYTHON%\\python.exe -m pip install -U pip" 8 | - "%PYTHON%\\python.exe -m pip install nose coverage codecov psutil pynput babel pillow pypiwin32" 9 | build: off 10 | test_script: 11 | - "%PYTHON%\\python.exe -m pip install ." 12 | - "%PYTHON%\\python.exe -m nose --with-coverage" 13 | after_test: 14 | - "%PYTHON%\\Scripts\\codecov.exe" 15 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ci: 3 | - travis 4 | - appveyor 5 | status: 6 | patch: false 7 | changes: false 8 | project: 9 | default: 10 | target: '80' 11 | comment: false 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | __pycache__ 4 | AUR 5 | .spyproject 6 | *.egg-info 7 | *.pyc 8 | .coverage 9 | htmlcov 10 | benchmark.py 11 | stats.txt 12 | _build 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | required: sudo 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | before_install: 9 | - "export DISPLAY=:99.0" 10 | - sudo systemctl start xvfb 11 | - sleep 3 12 | install: 13 | - sudo apt-get install python-tk python3-tk 14 | - python -m pip install nose coverage codecov psutil pynput babel 15 | script: 16 | - python -m pip install . 17 | - python -m nose --with-coverage 18 | after_success: 19 | - codecov 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt changelog 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | tkfilebrowser 2 | ============= 3 | 4 | |Release| |Linux| |Windows| |Travis| |Codecov| |License| |Doc| 5 | 6 | tkfilebrowser is an alternative to tkinter.filedialog that allows the 7 | user to select files or directories. The GUI is written with tkinter but 8 | the look is closer to GTK and the application uses GTK bookmarks (the 9 | one displayed in nautilus or thunar for instance). This filebrowser 10 | supports new directory creation and filtype filtering. 11 | 12 | This module contains a general ``FileBrowser`` class which implements the 13 | filebrowser and the following functions, similar to the one in filedialog: 14 | 15 | * ``askopenfilename`` that allow the selection of a single file 16 | 17 | * ``askopenfilenames`` that allow the selection of multiple files 18 | 19 | * ``askopendirname`` that allow the selection a single folder 20 | 21 | * ``askopendirnames`` that allow the selection of multiple folders 22 | 23 | * ``askopenpathname`` that allow the selection a single file or folder 24 | 25 | * ``askopenpathnames`` that allow the selection of multiple files and folders 26 | 27 | * ``asksaveasfilename`` that returns a single filename and give a warning if the file already exists 28 | 29 | 30 | The documentation is also available here: https://tkfilebrowser.readthedocs.io 31 | 32 | .. contents:: Table of Contents 33 | 34 | 35 | Requirements 36 | ------------ 37 | 38 | - Linux or Windows 39 | - Python 2.7 or 3.x 40 | 41 | And the python packages: 42 | 43 | - tkinter (included in the python distribution for Windows) 44 | - `psutil `_ 45 | - `babel `_ 46 | - `pywin32 `_ (Windows only) 47 | - `pillow `_ (only if tkinter.TkVersion < 8.6) 48 | 49 | 50 | Installation 51 | ------------ 52 | 53 | - Ubuntu: use the PPA `ppa:j-4321-i/ppa `__ 54 | 55 | :: 56 | 57 | $ sudo add-apt-repository ppa:j-4321-i/ppa 58 | $ sudo apt-get update 59 | $ sudo apt-get install python(3)-tkfilebrowser 60 | 61 | 62 | - Archlinux: 63 | 64 | the package is available on `AUR `__ 65 | 66 | 67 | - With pip: 68 | 69 | :: 70 | 71 | $ pip install tkfilebrowser 72 | 73 | 74 | Documentation 75 | ------------- 76 | 77 | * Optional keywords arguments common to each function 78 | 79 | - parent: parent window 80 | 81 | - title: the title of the filebrowser window 82 | 83 | - initialdir: directory whose content is initially displayed 84 | 85 | - initialfile: initially selected item (just the name, not the full path) 86 | 87 | - filetypes list: [("name", "\*.ext1|\*.ext2|.."), ...] 88 | only the files of given filetype will be displayed, 89 | e.g. to allow the user to switch between displaying only PNG or JPG 90 | pictures or dispalying all files: 91 | filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")] 92 | 93 | - okbuttontext: text displayed on the validate button, if None, the 94 | default text corresponding to the mode is used (either "Open" or "Save") 95 | 96 | - cancelbuttontext: text displayed on the button that cancels the 97 | selection. 98 | 99 | - foldercreation: enable the user to create new folders if True (default) 100 | 101 | * askopendirname 102 | 103 | Allow the user to choose a single directory. The absolute path of the 104 | chosen directory is returned. If the user cancels, an empty string is 105 | returned. 106 | 107 | * askopendirnames 108 | 109 | Allow the user to choose multiple directories. A tuple containing the absolute 110 | path of the chosen directories is returned. If the user cancels, 111 | an empty tuple is returned. 112 | 113 | * askopenfilename 114 | 115 | Allow the user to choose a single file. The absolute path of the 116 | chosen file is returned. If the user cancels, an empty string is 117 | returned. 118 | 119 | * askopenfilenames 120 | 121 | Allow the user to choose multiple files. A tuple containing the absolute 122 | path of the chosen files is returned. If the user cancels, 123 | an empty tuple is returned. 124 | 125 | * askopenpathname 126 | 127 | Allow the user to choose a single file or folder. The absolute path of the 128 | chosen item is returned. If the user cancels, an empty string is 129 | returned. 130 | 131 | * askopenpathnames 132 | 133 | Allow the user to choose multiple files and folders. A tuple containing the absolute 134 | path of the items is returned. If the user cancels, 135 | an empty tuple is returned. 136 | 137 | * asksaveasfilename 138 | 139 | Allow the user to choose a file path. The file may not exist but 140 | the path to its directory does. If the file already exists, the user 141 | is asked to confirm its replacement. 142 | 143 | Additional option: 144 | 145 | - defaultext: extension added to filename if none is given (default is none) 146 | 147 | 148 | Changelog 149 | --------- 150 | 151 | - tkfilebrowser 2.4.0 152 | * Add "openpath" mode to the ``FileBrowser`` to select both files and folders 153 | * Add ``askopenpathname()`` and ``askopenpathnames()`` to select path(s) 154 | 155 | - tkfilebrowser 2.3.2 156 | * Show networked drives on Windows 157 | * Fix click on root button in path bar 158 | 159 | - tkfilebrowser 2.3.1 160 | * Fix path bar navigation in Linux 161 | 162 | - tkfilebrowser 2.3.0 163 | * Make package compatible with Windows 164 | * Set initial focus on entry in save mode 165 | 166 | - tkfilebrowser 2.2.6 167 | * No longer reset path bar when clicking on a path button 168 | * Fix bug caused by broken links 169 | 170 | - tkfilebrowser 2.2.5 171 | * Add compatibility with Tk < 8.6.0 (requires PIL.ImageTk) 172 | * Add desktop icon in shortcuts 173 | * Fix handling of spaces in bookmarks 174 | * Fix bug due to spaces in recent file names 175 | 176 | - tkfilebrowser 2.2.4 177 | * Fix bug in desktop folder identification 178 | 179 | - tkfilebrowser 2.2.3 180 | * Fix FileNotFoundError if initialdir does not exist 181 | * Add Desktop in shortcuts (if found) 182 | * Improve filetype filtering 183 | 184 | - tkfilebrowser 2.2.2 185 | * Fix ValueError in after_cancel with Python 3.6.5 186 | 187 | - tkfilebrowser 2.2.1 188 | * Fix __main__.py for python 2 189 | 190 | - tkfilebrowser 2.2.0 191 | * Use babel instead of locale in order not to change the locale globally 192 | * Speed up (a little) folder content display 193 | * Improve example: add comparison with default dialogs 194 | * Add select all on Ctrl+A if multiple selection is enabled 195 | * Disable folder creation button if the user does not have write access 196 | * Improve extension management in save mode 197 | 198 | - tkfilebrowser 2.1.1 199 | * Fix error if LOCAL_PATH does not exists or is not writable 200 | 201 | - tkfilebrowser 2.1.0 202 | * Add compatibility with tkinter.filedialog keywords 'master' and 'defaultextension' 203 | * Change look of filetype selector 204 | * Fix bugs when navigating without displaying hidden files 205 | * Fix color alternance bug when hiding hidden files 206 | * Fix setup.py 207 | * Hide suggestion drop-down when nothing matches anymore 208 | 209 | - tkfilebrowser 2.0.0 210 | * Change package name to ``tkfilebrowser`` to respect PEP 8 211 | * Display error message when an issue occurs during folder creation 212 | * Cycle only through folders with key browsing in "opendir" mode 213 | * Complete only with folder names in "opendir" mode 214 | * Fix bug: grey/white color alternance not always respected 215 | * Add __main__.py with an example 216 | * Add "Recent files" shortcut 217 | * Make the text of the validate and cancel buttons customizable 218 | * Add possibility to disable new folder creation 219 | * Add python 2 support 220 | * Add horizontal scrollbar 221 | 222 | - tkFileBrowser 1.1.2 223 | * Add tooltips to display the full path of the shortcut if the mouse stays 224 | long enough over it. 225 | * Fix bug: style of browser treeview applied to parent 226 | 227 | - tkFileBrowser 1.1.1 228 | * Fix bug: key browsing did not work with capital letters 229 | * Add specific icons for symlinks 230 | * Add handling of symlinks, the real path is returned instead of the link path 231 | 232 | - tkFileBrowser 1.1.0 233 | * Fix bug concerning the initialfile argument 234 | * Add column sorting (by name, size, modification date) 235 | 236 | - tkFileBrowser 1.0.1 237 | * Set default filebrowser parent to None as for the usual filedialogs and messageboxes. 238 | 239 | - tkFileBrowser 1.0.0 240 | * Initial version 241 | 242 | 243 | Example 244 | ======= 245 | 246 | .. code:: python 247 | 248 | from tkfilebrowser import askopendirname, askopenfilenames, asksaveasfilename, askopenpathnames 249 | try: 250 | import tkinter as tk 251 | from tkinter import ttk 252 | from tkinter import filedialog 253 | except ImportError: 254 | import Tkinter as tk 255 | import ttk 256 | import tkFileDialog as filedialog 257 | 258 | root = tk.Tk() 259 | 260 | style = ttk.Style(root) 261 | style.theme_use("clam") 262 | root.configure(bg=style.lookup('TFrame', 'background')) 263 | 264 | def c_open_file_old(): 265 | rep = filedialog.askopenfilenames(parent=root, initialdir='/', initialfile='tmp', 266 | filetypes=[("PNG", "*.png"), 267 | ("JPEG", "*.jpg"), 268 | ("All files", "*")]) 269 | print(rep) 270 | 271 | 272 | def c_open_dir_old(): 273 | rep = filedialog.askdirectory(parent=root, initialdir='/tmp') 274 | print(rep) 275 | 276 | 277 | def c_save_old(): 278 | rep = filedialog.asksaveasfilename(parent=root, defaultextension=".png", 279 | initialdir='/tmp', initialfile='image.png', 280 | filetypes=[("PNG", "*.png"), 281 | ("JPEG", "*.jpg"), 282 | ("Text files", "*.txt"), 283 | ("All files", "*")]) 284 | print(rep) 285 | 286 | 287 | def c_open_file(): 288 | rep = askopenfilenames(parent=root, initialdir='/', initialfile='tmp', 289 | filetypes=[("Pictures", "*.png|*.jpg|*.JPG"), 290 | ("All files", "*")]) 291 | print(rep) 292 | 293 | 294 | def c_open_dir(): 295 | rep = askopendirname(parent=root, initialdir='/', initialfile='tmp') 296 | print(rep) 297 | 298 | 299 | def c_save(): 300 | rep = asksaveasfilename(parent=root, defaultext=".png", initialdir='/tmp', initialfile='image.png', 301 | filetypes=[("Pictures", "*.png|*.jpg|*.JPG"), 302 | ("Text files", "*.txt"), 303 | ("All files", "*")]) 304 | print(rep) 305 | 306 | 307 | def c_path(): 308 | rep = askopenpathnames(parent=root, initialdir='/', initialfile='tmp') 309 | print(rep) 310 | 311 | 312 | ttk.Label(root, text='Default dialogs').grid(row=0, column=0, padx=4, pady=4, sticky='ew') 313 | ttk.Label(root, text='tkfilebrowser dialogs').grid(row=0, column=1, padx=4, pady=4, sticky='ew') 314 | ttk.Button(root, text="Open files", command=c_open_file_old).grid(row=1, column=0, padx=4, pady=4, sticky='ew') 315 | ttk.Button(root, text="Open folder", command=c_open_dir_old).grid(row=2, column=0, padx=4, pady=4, sticky='ew') 316 | ttk.Button(root, text="Save file", command=c_save_old).grid(row=3, column=0, padx=4, pady=4, sticky='ew') 317 | ttk.Button(root, text="Open files", command=c_open_file).grid(row=1, column=1, padx=4, pady=4, sticky='ew') 318 | ttk.Button(root, text="Open folder", command=c_open_dir).grid(row=2, column=1, padx=4, pady=4, sticky='ew') 319 | ttk.Button(root, text="Save file", command=c_save).grid(row=3, column=1, padx=4, pady=4, sticky='ew') 320 | ttk.Button(root, text="Open paths", command=c_path).grid(row=4, column=1, padx=4, pady=4, sticky='ew') 321 | 322 | root.mainloop() 323 | 324 | 325 | .. |Release| image:: https://badge.fury.io/py/tkfilebrowser.svg 326 | :alt: Latest Release 327 | :target: https://pypi.org/project/tkfilebrowser/ 328 | .. |Linux| image:: https://img.shields.io/badge/platform-Linux-blue.svg 329 | :alt: Platform Linux 330 | .. |Windows| image:: https://img.shields.io/badge/platform-Windows-blue.svg 331 | :alt: Platform Windows 332 | .. |Travis| image:: https://travis-ci.org/j4321/tkFileBrowser.svg?branch=master 333 | :target: https://travis-ci.org/j4321/tkFileBrowser 334 | :alt: Travis CI Build Status 335 | .. |Codecov| image:: https://codecov.io/gh/j4321/tkFileBrowser/branch/master/graph/badge.svg 336 | :target: https://codecov.io/gh/j4321/tkFileBrowser 337 | :alt: Code coverage 338 | .. |License| image:: https://img.shields.io/github/license/j4321/tkFileBrowser.svg 339 | :target: https://www.gnu.org/licenses/gpl-3.0.en.html 340 | :alt: License 341 | .. |Doc| image:: https://readthedocs.org/projects/tkfilebrowser/badge/?version=latest 342 | :target: https://tkfilebrowser.readthedocs.io/en/latest/?badge=latest 343 | :alt: Documentation Status 344 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. currentmodule:: tkfilebrowser 5 | 6 | tkfilebrowser 2.4.0 7 | ------------------- 8 | 9 | * Add "openpath" mode to the :class:`FileBrowser` to select both files and folders 10 | * Add :meth:`askopenpathname` and :meth:`askopenpathnames` to select path(s) 11 | 12 | tkfilebrowser 2.3.1 13 | ------------------- 14 | 15 | * Fix path bar navigation in Linux 16 | * Show networked drives on Windows 17 | 18 | tkfilebrowser 2.3.0 19 | ------------------- 20 | 21 | * Make package compatible with Windows 22 | * Set initial focus on entry in save mode 23 | 24 | tkfilebrowser 2.2.6 25 | ------------------- 26 | 27 | * No longer reset path bar when clicking on a path button 28 | * Fix bug caused by broken links 29 | 30 | tkfilebrowser 2.2.5 31 | ------------------- 32 | 33 | * Add compatibility with Tk < 8.6.0 (requires :mod:`PIL.ImageTk`) 34 | * Add desktop icon in shortcuts 35 | * Fix handling of spaces in bookmarks 36 | * Fix bug due to spaces in recent file names 37 | 38 | tkfilebrowser 2.2.4 39 | ------------------- 40 | * Fix bug in desktop folder identification 41 | 42 | tkfilebrowser 2.2.3 43 | ------------------- 44 | 45 | * Fix :obj:`FileNotFoundError` if initialdir does not exist 46 | * Add Desktop in shortcuts (if found) 47 | * Improve filetype filtering 48 | 49 | tkfilebrowser 2.2.2 50 | ------------------- 51 | 52 | * Fix :obj:`ValueError` in after_cancel with Python 3.6.5 53 | 54 | tkfilebrowser 2.2.1 55 | ------------------- 56 | 57 | * Fix __main__.py for python 2 58 | 59 | tkfilebrowser 2.2.0 60 | ------------------- 61 | 62 | * Use :mod:`babel` instead of locale in order not to change the locale globally 63 | * Speed up (a little) folder content display 64 | * Improve example: add comparison with default dialogs 65 | * Add select all on Ctrl+A if multiple selection is enabled 66 | * Disable folder creation button if the user does not have write access 67 | * Improve extension management in "save" mode 68 | 69 | tkfilebrowser 2.1.1 70 | ------------------- 71 | 72 | * Fix error if :obj:`LOCAL_PATH` does not exists or is not writable 73 | 74 | tkfilebrowser 2.1.0 75 | ------------------- 76 | 77 | * Add compatibility with :mod:`tkinter.filedialog` keywords *master* and *defaultextension* 78 | * Change look of filetype selector 79 | * Fix bugs when navigating without displaying hidden files 80 | * Fix color alternance bug when hiding hidden files 81 | * Fix setup.py 82 | * Hide suggestion drop-down when nothing matches anymore 83 | 84 | tkfilebrowser 2.0.0 85 | ------------------- 86 | 87 | * Change package name to :mod:`tkfilebrowser` to respect `PEP 8 `_ 88 | * Display error message when an issue occurs during folder creation 89 | * Cycle only through folders with key browsing in "opendir" mode 90 | * Complete only with folder names in "opendir" mode 91 | * Fix bug: grey/white color alternance not always respected 92 | * Add __main__.py with an example 93 | * Add "Recent files" shortcut 94 | * Make the text of the validate and cancel buttons customizable 95 | * Add possibility to disable new folder creation 96 | * Add python 2 support 97 | * Add horizontal scrollbar 98 | 99 | tkFileBrowser 1.1.2 100 | ------------------- 101 | 102 | * Add tooltips to display the full path of the shortcut if the mouse stays long enough over it. 103 | * Fix bug: style of browser treeview applied to parent 104 | 105 | tkFileBrowser 1.1.1 106 | ------------------- 107 | 108 | * Fix bug: key browsing did not work with capital letters 109 | * Add specific icons for symlinks 110 | * Add handling of symlinks, the real path is returned instead of the link path 111 | 112 | tkFileBrowser 1.1.0 113 | ------------------- 114 | 115 | * Fix bug concerning the *initialfile* argument 116 | * Add column sorting (by name, size, modification date) 117 | 118 | tkFileBrowser 1.0.1 119 | ------------------- 120 | 121 | * Set default :class:`Filebrowser` parent to :obj:`None` as for the usual filedialogs and messageboxes. 122 | 123 | tkFileBrowser 1.0.0 124 | ------------------- 125 | 126 | * Initial version 127 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('..')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'tkfilebrowser' 23 | copyright = '2018, Juliette Monsel' 24 | author = 'Juliette Monsel' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '2.2.5' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.viewcode', 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = '.rst' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | # 61 | # This is also used if you do content translation via gettext catalogs. 62 | # Usually you set "language" from the command line for these cases. 63 | language = None 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | # This pattern also affects html_static_path and html_extra_path . 68 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 69 | 70 | # The name of the Pygments (syntax highlighting) style to use. 71 | pygments_style = 'tango' 72 | 73 | 74 | # -- Options for HTML output ------------------------------------------------- 75 | 76 | # The theme to use for HTML and HTML Help pages. See the documentation for 77 | # a list of builtin themes. 78 | # 79 | html_theme = 'sphinx_rtd_theme' 80 | 81 | # Theme options are theme-specific and customize the look and feel of a theme 82 | # further. For a list of options available for each theme, see the 83 | # documentation. 84 | # 85 | # html_theme_options = {} 86 | 87 | # Add any paths that contain custom static files (such as style sheets) here, 88 | # relative to this directory. They are copied after the builtin static files, 89 | # so a file named "default.css" will overwrite the builtin "default.css". 90 | html_static_path = ['_static'] 91 | 92 | # Custom sidebar templates, must be a dictionary that maps document names 93 | # to template names. 94 | # 95 | # The default sidebars (for documents that don't match any pattern) are 96 | # defined by theme itself. Builtin themes are using these templates by 97 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 98 | # 'searchbox.html']``. 99 | # 100 | # html_sidebars = {} 101 | 102 | 103 | # -- Options for HTMLHelp output --------------------------------------------- 104 | 105 | # Output file base name for HTML help builder. 106 | htmlhelp_basename = 'tkfilebrowserdoc' 107 | 108 | 109 | # -- Options for LaTeX output ------------------------------------------------ 110 | 111 | latex_elements = { 112 | # The paper size ('letterpaper' or 'a4paper'). 113 | # 114 | # 'papersize': 'letterpaper', 115 | 116 | # The font size ('10pt', '11pt' or '12pt'). 117 | # 118 | # 'pointsize': '10pt', 119 | 120 | # Additional stuff for the LaTeX preamble. 121 | # 122 | # 'preamble': '', 123 | 124 | # Latex figure (float) alignment 125 | # 126 | # 'figure_align': 'htbp', 127 | } 128 | 129 | # Grouping the document tree into LaTeX files. List of tuples 130 | # (source start file, target name, title, 131 | # author, documentclass [howto, manual, or own class]). 132 | latex_documents = [ 133 | (master_doc, 'tkfilebrowser.tex', 'tkfilebrowser Documentation', 134 | 'Juliette Monsel', 'manual'), 135 | ] 136 | 137 | 138 | # -- Options for manual page output ------------------------------------------ 139 | 140 | # One entry per manual page. List of tuples 141 | # (source start file, name, description, authors, manual section). 142 | man_pages = [ 143 | (master_doc, 'tkfilebrowser', 'tkfilebrowser Documentation', 144 | [author], 1) 145 | ] 146 | 147 | 148 | # -- Options for Texinfo output ---------------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | (master_doc, 'tkfilebrowser', 'tkfilebrowser Documentation', 155 | author, 'tkfilebrowser', 'One line description of project.', 156 | 'Miscellaneous'), 157 | ] 158 | 159 | 160 | # -- Options for Epub output ------------------------------------------------- 161 | 162 | # Bibliographic Dublin Core info. 163 | epub_title = project 164 | 165 | # The unique identifier of the text. This can be a ISBN number 166 | # or the project homepage. 167 | # 168 | # epub_identifier = '' 169 | 170 | # A unique identification for the text. 171 | # 172 | # epub_uid = '' 173 | 174 | # A list of files that should not be packed into the epub file. 175 | epub_exclude_files = ['search.html'] 176 | -------------------------------------------------------------------------------- /docs/documentation.rst: -------------------------------------------------------------------------------- 1 | Documentation 2 | ============= 3 | 4 | .. currentmodule:: tkfilebrowser 5 | 6 | askopendirname 7 | -------------- 8 | .. autofunction:: askopendirname 9 | 10 | askopendirnames 11 | --------------- 12 | .. autofunction:: askopendirnames 13 | 14 | askopenfilename 15 | --------------- 16 | .. autofunction:: askopenfilename 17 | 18 | askopenfilenames 19 | ---------------- 20 | .. autofunction:: askopenfilenames 21 | 22 | askopenpathname 23 | --------------- 24 | .. autofunction:: askopenpathname 25 | 26 | askopenpathnames 27 | ---------------- 28 | .. autofunction:: askopenpathnames 29 | 30 | asksaveasfilename 31 | ----------------- 32 | .. autofunction:: asksaveasfilename 33 | 34 | 35 | FileBrowser 36 | ----------- 37 | 38 | .. autoclass:: FileBrowser 39 | :members: 40 | 41 | .. automethod:: __init__ 42 | -------------------------------------------------------------------------------- /docs/example.rst: -------------------------------------------------------------------------------- 1 | Example 2 | ======= 3 | 4 | .. code:: python 5 | 6 | try: 7 | import tkinter as tk 8 | import tkinter.ttk as ttk 9 | from tkinter import filedialog 10 | except ImportError: 11 | import Tkinter as tk 12 | import ttk 13 | import tkFileDialog as filedialog 14 | from tkfilebrowser import askopendirname, askopenfilenames, asksaveasfilename, askopenpathnames 15 | 16 | 17 | root = tk.Tk() 18 | 19 | style = ttk.Style(root) 20 | style.theme_use("clam") 21 | 22 | 23 | def c_open_file_old(): 24 | rep = filedialog.askopenfilenames(parent=root, 25 | initialdir='/', 26 | initialfile='tmp', 27 | filetypes=[("PNG", "*.png"), 28 | ("JPEG", "*.jpg"), 29 | ("All files", "*")]) 30 | print(rep) 31 | 32 | 33 | def c_open_dir_old(): 34 | rep = filedialog.askdirectory(parent=root, initialdir='/tmp') 35 | print(rep) 36 | 37 | 38 | def c_save_old(): 39 | rep = filedialog.asksaveasfilename(parent=root, 40 | defaultextension=".png", 41 | initialdir='/tmp', 42 | initialfile='image.png', 43 | filetypes=[("PNG", "*.png"), 44 | ("JPEG", "*.jpg"), 45 | ("All files", "*")]) 46 | print(rep) 47 | 48 | 49 | def c_open_file(): 50 | rep = askopenfilenames(parent=root, 51 | initialdir='/', 52 | initialfile='tmp', 53 | filetypes=[("Pictures", "*.png|*.jpg|*.JPG"), 54 | ("All files", "*")]) 55 | print(rep) 56 | 57 | 58 | def c_open_dir(): 59 | rep = askopendirname(parent=root, 60 | initialdir='/', 61 | initialfile='tmp') 62 | print(rep) 63 | 64 | 65 | def c_save(): 66 | rep = asksaveasfilename(parent=root, 67 | defaultext=".png", 68 | initialdir='/tmp', 69 | initialfile='image.png', 70 | filetypes=[("Pictures", "*.png|*.jpg|*.JPG"), 71 | ("All files", "*")]) 72 | print(rep) 73 | 74 | 75 | def c_path(): 76 | rep = askopenpathnames(parent=root, initialdir='/', initialfile='tmp') 77 | print(rep) 78 | 79 | 80 | ttk.Label(root, text='Default dialogs').grid(row=0, column=0, 81 | padx=4, pady=4, 82 | sticky='ew') 83 | ttk.Label(root, text='tkfilebrowser dialogs').grid(row=0, column=1, 84 | padx=4, pady=4, 85 | sticky='ew') 86 | ttk.Button(root, text="Open files", command=c_open_file_old).grid(row=1, column=0, 87 | padx=4, pady=4, 88 | sticky='ew') 89 | ttk.Button(root, text="Open folder", command=c_open_dir_old).grid(row=2, column=0, 90 | padx=4, pady=4, 91 | sticky='ew') 92 | ttk.Button(root, text="Save file", command=c_save_old).grid(row=3, column=0, 93 | padx=4, pady=4, 94 | sticky='ew') 95 | ttk.Button(root, text="Open files", command=c_open_file).grid(row=1, column=1, 96 | padx=4, pady=4, 97 | sticky='ew') 98 | ttk.Button(root, text="Open folder", command=c_open_dir).grid(row=2, column=1, 99 | padx=4, pady=4, 100 | sticky='ew') 101 | ttk.Button(root, text="Save file", command=c_save).grid(row=3, column=1, 102 | padx=4, pady=4, 103 | sticky='ew') 104 | ttk.Button(root, text="Open paths", command=c_path).grid(row=4, column=1, 105 | padx=4, pady=4, 106 | sticky='ew') 107 | 108 | 109 | root.mainloop() 110 | -------------------------------------------------------------------------------- /docs/genindex.rst: -------------------------------------------------------------------------------- 1 | Index 2 | ===== 3 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. tkfilebrowser documentation master file, created by 2 | sphinx-quickstart on Mon Sep 24 22:37:52 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | tkfilebrowser 7 | ============= 8 | 9 | |Release| |Linux| |Travis| |Codecov| |License| |Doc| 10 | 11 | tkfilebrowser is an alternative to tkinter.filedialog that allows the 12 | user to select files or directories. The GUI is written with tkinter but 13 | the look is closer to GTK and the application uses GTK bookmarks (the 14 | one displayed in nautilus or thunar for instance). This filebrowser 15 | supports new directory creation and filtype filtering. 16 | 17 | This module contains a general :class:`~tkfilebrowser.FileBrowser` class which implements the 18 | filebrowser and the following functions, similar to the one in filedialog: 19 | 20 | * :func:`~tkfilebrowser.askopenfilename` that allow the selection of a single file 21 | 22 | * :func:`~tkfilebrowser.askopenfilenames` that allow the selection of multiple files 23 | 24 | * :func:`~tkfilebrowser.askopendirname` that allow the selection a single folder 25 | 26 | * :func:`~tkfilebrowser.askopendirnames` that allow the selection of multiple folders 27 | 28 | * :func:`~tkfilebrowser.askopendirname` that allow the selection a single file or folder 29 | 30 | * :func:`~tkfilebrowser.askopendirnames` that allow the selection of multiple files and folders 31 | 32 | * :func:`~tkfilebrowser.asksaveasfilename` that returns a single filename and give a warning if the file already exists 33 | 34 | Project page: https://github.com/j4321/tkFileBrowser 35 | 36 | .. toctree:: 37 | :maxdepth: 1 38 | :caption: Contents: 39 | 40 | installation 41 | example 42 | documentation 43 | changelog 44 | genindex 45 | 46 | 47 | .. |Release| image:: https://badge.fury.io/py/tkfilebrowser.svg 48 | :alt: Latest Release 49 | :target: https://pypi.org/project/tkfilebrowser/ 50 | .. |Linux| image:: https://img.shields.io/badge/platform-Linux-blue.svg 51 | :alt: Platform 52 | .. |Travis| image:: https://travis-ci.org/j4321/tkFileBrowser.svg?branch=master 53 | :target: https://travis-ci.org/j4321/tkFileBrowser 54 | :alt: Travis CI Build Status 55 | .. |Codecov| image:: https://codecov.io/gh/j4321/tkFileBrowser/branch/master/graph/badge.svg 56 | :target: https://codecov.io/gh/j4321/tkFileBrowser 57 | :alt: Code coverage 58 | .. |License| image:: https://img.shields.io/github/license/j4321/tkFileBrowser.svg 59 | :target: https://www.gnu.org/licenses/gpl-3.0.en.html 60 | :alt: License 61 | .. |Doc| image:: https://readthedocs.org/projects/tkfilebrowser/badge/?version=latest 62 | :target: https://tkfilebrowser.readthedocs.io/en/latest/?badge=latest 63 | :alt: Documentation Status 64 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Requirements 5 | ------------ 6 | 7 | - Linux or Windows 8 | - Python 2.7 or 3.x 9 | 10 | And the python packages: 11 | 12 | - tkinter (included in the python distribution for Windows) 13 | - psutil 14 | - babel 15 | - pywin32 (Windows only) 16 | - pillow (only if tkinter.TkVersion < 8.6) 17 | 18 | Install 19 | ------- 20 | 21 | - Ubuntu: use the PPA `ppa:j-4321-i/ppa `__ 22 | 23 | :: 24 | 25 | $ sudo add-apt-repository ppa:j-4321-i/ppa 26 | $ sudo apt-get update 27 | $ sudo apt-get install python(3)-tkfilebrowser 28 | 29 | 30 | - Archlinux: 31 | 32 | the package is available on `AUR `__ 33 | 34 | 35 | - With pip: 36 | 37 | :: 38 | 39 | $ pip install tkfilebrowser 40 | 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | from codecs import open 7 | from os import path, name 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | 11 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 12 | long_description = f.read() 13 | 14 | 15 | 16 | setup(name='tkfilebrowser', 17 | version='2.3.2', 18 | description='File browser for Tkinter, alternative to tkinter.filedialog in linux with GTK bookmarks support.', 19 | long_description=long_description, 20 | url='https://github.com/j4321/tkFileBrowser', 21 | author='Juliette Monsel', 22 | author_email='j_4321@protonmail.com', 23 | license='GPLv3', 24 | classifiers=['Development Status :: 5 - Production/Stable', 25 | 'Intended Audience :: Developers', 26 | 'Topic :: Software Development :: Widget Sets', 27 | 'Topic :: Software Development :: Libraries :: Python Modules', 28 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.4', 32 | 'Programming Language :: Python :: 3.5', 33 | 'Programming Language :: Python :: 3.6', 34 | 'Programming Language :: Python :: 3.7', 35 | 'Natural Language :: English', 36 | 'Natural Language :: French', 37 | 'Operating System :: POSIX :: Linux', 38 | 'Operating System :: Microsoft :: Windows'], 39 | py_modules=["tkfilebrowser.autoscrollbar", 40 | "tkfilebrowser.constants", 41 | "tkfilebrowser.filebrowser", 42 | "tkfilebrowser.functions", 43 | "tkfilebrowser.path_button", 44 | "tkfilebrowser.recent_files", 45 | "tkfilebrowser.tooltip"], 46 | keywords=['tkinter', 'filedialog', 'filebrowser'], 47 | packages=["tkfilebrowser"], 48 | package_data={"tkfilebrowser": ["images/*"]}, 49 | install_requires=["psutil", "babel"] + (['pypiwin32'] if name == 'nt' else []), 50 | extras_require={'tk<8.6.0': 'Pillow'}) 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.base_widget_testcase import BaseWidgetTest, TestEvent 2 | -------------------------------------------------------------------------------- /tests/base_widget_testcase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | try: 3 | import Tkinter as tk 4 | except ImportError: 5 | import tkinter as tk 6 | 7 | 8 | class BaseWidgetTest(unittest.TestCase): 9 | def setUp(self): 10 | self.window = tk.Toplevel() 11 | self.window.update() 12 | 13 | def tearDown(self): 14 | self.window.update() 15 | self.window.destroy() 16 | 17 | 18 | class TestEvent: 19 | """Fake event for testing.""" 20 | def __init__(self, **kwargs): 21 | self._prop = kwargs 22 | 23 | def __getattr__(self, attr): 24 | if attr not in self._prop: 25 | raise AttributeError("TestEvent has no attribute %s." % attr) 26 | else: 27 | return self._prop[attr] 28 | -------------------------------------------------------------------------------- /tests/test_autoscrollbar.py: -------------------------------------------------------------------------------- 1 | from tkfilebrowser.autoscrollbar import AutoScrollbar 2 | from tests import BaseWidgetTest 3 | try: 4 | import Tkinter as tk 5 | except ImportError: 6 | import tkinter as tk 7 | 8 | 9 | class TestAutoScrollbar(BaseWidgetTest): 10 | def test_autoscrollbar_init(self): 11 | AutoScrollbar(self.window) 12 | self.window.update() 13 | 14 | def test_autoscrollbar_methods(self): 15 | scroll = AutoScrollbar(self.window, orient='vertical') 16 | # pack layout 17 | with self.assertRaises(tk.TclError): 18 | scroll.pack(side='right', fill='y') 19 | self.window.update() 20 | # place layout 21 | with self.assertRaises(tk.TclError): 22 | scroll.place(anchor='ne', relx=1, rely=0, relheight=1) 23 | self.window.update() 24 | # grid layout 25 | scroll.grid(row=0, column=1, sticky='ns') 26 | self.window.update() 27 | scroll.set(-0.1, 1.1) 28 | self.window.update() 29 | self.assertFalse(scroll.winfo_ismapped()) 30 | scroll.set(0.1, 0.8) 31 | self.window.update() 32 | self.window.update() 33 | self.assertTrue(scroll.winfo_ismapped()) 34 | -------------------------------------------------------------------------------- /tests/test_filebrowser.py: -------------------------------------------------------------------------------- 1 | from tkfilebrowser.filebrowser import FileBrowser 2 | from tests import BaseWidgetTest, TestEvent 3 | try: 4 | import ttk 5 | except ImportError: 6 | from tkinter import ttk 7 | import os 8 | from pynput.keyboard import Key, Controller 9 | 10 | 11 | class TestFileBrowser(BaseWidgetTest): 12 | def test_filebrowser_openpath(self): 13 | # --- multiple selection 14 | path = os.path.expanduser('~') 15 | fb = FileBrowser(self.window, initialdir=path, initialfile="test", mode="openpath", 16 | multiple_selection=True, 17 | title="Test", filetypes=[], 18 | okbuttontext=None, cancelbuttontext="Cancel", 19 | foldercreation=False) 20 | self.window.update() 21 | fb.right_tree.focus_force() 22 | self.window.update() 23 | fb.event_generate('') 24 | self.window.update() 25 | self.window.update_idletasks() 26 | fb.validate() 27 | walk = os.walk(path) 28 | root, dirs, files = walk.send(None) 29 | res = list(fb.get_result()) 30 | res.sort() 31 | dirs = [os.path.realpath(os.path.join(root, d)) for d in dirs] 32 | files = [os.path.realpath(os.path.join(root, f)) for f in files] 33 | paths = dirs + files 34 | paths.sort() 35 | self.assertEqual(res, paths) 36 | # --- single selection 37 | fb = FileBrowser(self.window, initialdir=".", initialfile="test", mode="openpath", 38 | multiple_selection=False, 39 | title="Test", filetypes=[], 40 | okbuttontext=None, cancelbuttontext="Cancel", 41 | foldercreation=False) 42 | self.window.update() 43 | fb.validate() 44 | self.assertEqual(fb.get_result(), '') 45 | fb = FileBrowser(self.window, initialdir=".", initialfile="test", mode="openpath", 46 | multiple_selection=False, 47 | title="Test", filetypes=[], 48 | okbuttontext=None, cancelbuttontext="Cancel", 49 | foldercreation=False) 50 | self.window.update() 51 | files = fb.right_tree.tag_has('file') 52 | if files: 53 | fb.right_tree.selection_set(files[0]) 54 | fb.validate() 55 | self.assertTrue(os.path.isfile(fb.get_result())) 56 | else: 57 | fb.validate() 58 | self.assertEqual(fb.get_result(), '') 59 | fb = FileBrowser(self.window, initialdir=".", initialfile="test", mode="openpath", 60 | multiple_selection=False, 61 | title="Test", filetypes=[], 62 | okbuttontext=None, cancelbuttontext="Cancel", 63 | foldercreation=False) 64 | self.window.update() 65 | dirs = fb.right_tree.tag_has('folder') 66 | if dirs: 67 | fb.right_tree.selection_set(dirs[0]) 68 | fb.validate() 69 | self.assertTrue(os.path.isdir(fb.get_result())) 70 | else: 71 | fb.validate() 72 | self.assertEqual(fb.get_result(), '') 73 | 74 | def test_filebrowser_opendir(self): 75 | # --- multiple selection 76 | path = os.path.expanduser('~') 77 | fb = FileBrowser(self.window, initialdir=path, initialfile="test", mode="opendir", 78 | multiple_selection=True, defaultext=".png", 79 | title="Test", filetypes=[], 80 | okbuttontext=None, cancelbuttontext="Cancel", 81 | foldercreation=False) 82 | self.window.update() 83 | fb.right_tree.focus_force() 84 | self.window.update() 85 | fb.event_generate('') 86 | self.window.update() 87 | self.window.update_idletasks() 88 | fb.validate() 89 | walk = os.walk(path) 90 | root, dirs, _ = walk.send(None) 91 | res = list(fb.get_result()) 92 | res.sort() 93 | dirs = [os.path.realpath(os.path.join(root, d)) for d in dirs] 94 | dirs.sort() 95 | self.assertEqual(res, dirs) 96 | # --- single selection 97 | fb = FileBrowser(self.window, initialdir=".", initialfile="test", mode="opendir", 98 | multiple_selection=False, defaultext=".png", 99 | title="Test", filetypes=[], 100 | okbuttontext=None, cancelbuttontext="Cancel", 101 | foldercreation=False) 102 | self.window.update() 103 | fb.validate() 104 | self.assertTrue(os.path.isdir(fb.get_result())) 105 | 106 | def test_filebrowser_openfile(self): 107 | # --- multiple selection 108 | path = os.path.expanduser('~') 109 | fb = FileBrowser(self.window, initialdir=path, initialfile="test", mode="openfile", 110 | multiple_selection=True, defaultext=".png", 111 | title="Test", filetypes=[], 112 | okbuttontext=None, cancelbuttontext="Cancel", 113 | foldercreation=False) 114 | self.window.update() 115 | fb.right_tree.focus_force() 116 | self.window.update() 117 | fb.event_generate('') 118 | self.window.update() 119 | self.window.update_idletasks() 120 | fb.validate() 121 | walk = os.walk(path) 122 | root, _, files = walk.send(None) 123 | res = list(fb.get_result()) 124 | res.sort() 125 | files = [os.path.realpath(os.path.join(root, f)) for f in files] 126 | files.sort() 127 | self.assertEqual(res, files) 128 | # --- single selection 129 | fb = FileBrowser(self.window, initialdir=".", initialfile="test", mode="openfile", 130 | multiple_selection=False, defaultext="", 131 | title="Test", filetypes=[("PNG", '*.png'), ('ALL', '*')], 132 | okbuttontext=None, cancelbuttontext="Cancel", 133 | foldercreation=False) 134 | self.window.update() 135 | fb.validate() 136 | self.assertEqual(fb.get_result(), '') 137 | fb = FileBrowser(self.window, initialdir=".", initialfile="test", mode="openfile", 138 | multiple_selection=False, defaultext="", 139 | title="Test", filetypes=[("PNG", '*.png'), ('ALL', '*')], 140 | okbuttontext=None, cancelbuttontext="Cancel", 141 | foldercreation=False) 142 | self.window.update() 143 | fb.validate() 144 | self.assertEqual(fb.get_result(), '') 145 | fb = FileBrowser(self.window, initialdir=".", initialfile="test", mode="openfile", 146 | multiple_selection=False, defaultext="", 147 | title="Test", filetypes=[], 148 | okbuttontext=None, cancelbuttontext="Cancel", 149 | foldercreation=False) 150 | self.window.update() 151 | # walk = os.walk(os.path.abspath(".")) 152 | # root, _, files = walk.send(None) 153 | files = fb.right_tree.tag_has('file') 154 | if files: 155 | fb.right_tree.selection_set(files[0]) 156 | fb.validate() 157 | self.assertTrue(os.path.isfile(fb.get_result())) 158 | else: 159 | fb.validate() 160 | self.assertEqual(fb.get_result(), '') 161 | 162 | def test_filebrowser_save(self): 163 | fb = FileBrowser(self.window, initialdir="/", initialfile="test", mode="save", 164 | multiple_selection=True, defaultext=".png", 165 | title="Test", filetypes=[("PNG", '*.png'), ('ALL', '*')], 166 | okbuttontext=None, cancelbuttontext="Cancel", 167 | foldercreation=True) 168 | self.window.update() 169 | fb.validate() 170 | self.assertEqual(os.path.abspath(fb.get_result()), 171 | os.path.abspath('/test.png')) 172 | fb = FileBrowser(self.window, initialdir="/", initialfile="test.png", mode="save", 173 | filetypes=[("PNG", '*.png|*.PNG'), ("JPG", '*.jpg|*.JPG'), 174 | ('ALL', '*')]) 175 | self.window.update() 176 | self.assertEqual(fb.entry.get(), "test.png") 177 | fb.filetype.set('JPG') 178 | self.window.update() 179 | self.assertEqual(fb.entry.get(), "test.jpg") 180 | fb.filetype.set('ALL') 181 | self.window.update() 182 | self.assertEqual(fb.entry.get(), "test.jpg") 183 | fb.filetype.set('PNG') 184 | self.window.update() 185 | self.assertEqual(fb.entry.get(), "test.png") 186 | fb.entry.delete(0, 'end') 187 | fb.entry.insert(0, "test.JPG") 188 | fb.filetype.set('JPG') 189 | self.window.update() 190 | self.assertEqual(fb.entry.get(), "test.JPG") 191 | 192 | def test_filebrowser_keybrowse(self): 193 | # --- openfile 194 | fb = FileBrowser(self.window, initialdir="/", mode="openfile", 195 | multiple_selection=True) 196 | if not fb.hide: 197 | fb.toggle_hidden() 198 | self.window.update() 199 | fb.right_tree.focus_force() 200 | self.window.update() 201 | ch = fb.right_tree.get_children('') 202 | letters = [fb.right_tree.item(c, 'text')[0].lower() for c in ch] 203 | i = 65 204 | while chr(i).lower() in letters: 205 | i += 1 206 | letter = chr(i).lower() 207 | keyboard = Controller() 208 | if letter.isalnum(): 209 | fb.right_tree.focus_force() 210 | keyboard.press(letter) 211 | keyboard.release(letter) 212 | self.window.update() 213 | self.assertTrue(fb.key_browse_entry.winfo_ismapped()) 214 | self.assertEqual(fb.key_browse_entry.get().lower(), letter) 215 | fb.right_tree.event_generate('') 216 | self.window.update() 217 | self.assertFalse(fb.key_browse_entry.winfo_ismapped()) 218 | if ch: 219 | fb.right_tree.focus_force() 220 | letter = fb.right_tree.item(ch[0], 'text')[0] 221 | keyboard.press(letter) 222 | keyboard.release(letter) 223 | self.window.update() 224 | self.assertTrue(fb.key_browse_entry.winfo_ismapped()) 225 | self.assertEqual(fb.key_browse_entry.get().lower(), letter) 226 | self.assertEqual(fb.right_tree.selection(), (ch[0],)) 227 | l = [c for c in ch if fb.right_tree.item(c, 'text')[0] == letter] 228 | fb.key_browse_entry.focus_force() 229 | fb.key_browse_entry.event_generate('') 230 | self.window.update() 231 | if len(l) > 1: 232 | self.assertEqual(tuple(fb.right_tree.selection()), (l[1],)) 233 | else: 234 | self.assertEqual(tuple(fb.right_tree.selection()), (l[0],)) 235 | fb.key_browse_entry.focus_force() 236 | fb.key_browse_entry.event_generate('') 237 | self.window.update() 238 | self.assertEqual(tuple(fb.right_tree.selection()), (l[0],)) 239 | fb.key_browse_entry.focus_force() 240 | fb.key_browse_entry.event_generate('') 241 | self.window.update() 242 | self.assertFalse(fb.key_browse_entry.winfo_ismapped()) 243 | fb.right_tree.focus_force() 244 | keyboard.press(letter) 245 | keyboard.release(letter) 246 | self.window.update() 247 | fb.right_tree.event_generate('') 248 | self.window.update() 249 | item = os.path.realpath(ch[0]) 250 | if os.path.isdir(item): 251 | self.assertEqual(fb.history[-1], ch[0]) 252 | else: 253 | self.assertEqual(fb.get_result(), (item,)) 254 | 255 | # --- opendir 256 | fb = FileBrowser(self.window, initialdir="/", mode="opendir", 257 | multiple_selection=True) 258 | self.window.update() 259 | fb.right_tree.focus_force() 260 | self.window.update() 261 | ch = fb.right_tree.tag_has('folder') 262 | letters = [fb.right_tree.item(c, 'text')[0].lower() for c in ch] 263 | i = 65 264 | while chr(i).lower() in letters: 265 | i += 1 266 | letter = chr(i).lower() 267 | if letter.isalnum(): 268 | fb.right_tree.focus_force() 269 | keyboard.press(letter) 270 | keyboard.release(letter) 271 | self.window.update() 272 | self.assertTrue(fb.key_browse_entry.winfo_ismapped()) 273 | self.assertEqual(fb.key_browse_entry.get(), letter) 274 | fb.right_tree.event_generate('') 275 | self.window.update() 276 | self.assertEqual(fb.get_result(), (os.path.abspath('/'),)) 277 | fb = FileBrowser(self.window, initialdir="/", mode="opendir", 278 | multiple_selection=True) 279 | self.window.update() 280 | fb.right_tree.focus_force() 281 | if ch: 282 | letter = fb.right_tree.item(ch[-1], 'text')[0].lower() 283 | l = [c for c in ch if fb.right_tree.item(c, 'text')[0].lower() == letter] 284 | fb.right_tree.focus_force() 285 | keyboard.press(letter) 286 | keyboard.release(letter) 287 | self.window.update() 288 | self.assertTrue(fb.key_browse_entry.winfo_ismapped()) 289 | self.assertEqual(fb.key_browse_entry.get(), letter) 290 | self.assertEqual(fb.right_tree.selection(), (l[0],)) 291 | fb.key_browse_entry.focus_force() 292 | fb.key_browse_entry.event_generate('') 293 | self.window.update() 294 | if len(l) > 1: 295 | self.assertEqual(tuple(fb.right_tree.selection()), (l[1],)) 296 | else: 297 | self.assertEqual(tuple(fb.right_tree.selection()), (l[0],)) 298 | fb.key_browse_entry.focus_force() 299 | fb.key_browse_entry.event_generate('') 300 | self.window.update() 301 | self.assertEqual(tuple(fb.right_tree.selection()), (l[0],)) 302 | fb.key_browse_entry.focus_force() 303 | fb.key_browse_entry.event_generate('') 304 | self.window.update() 305 | self.assertFalse(fb.key_browse_entry.winfo_ismapped()) 306 | fb.right_tree.focus_force() 307 | keyboard.press(letter) 308 | keyboard.release(letter) 309 | self.window.update() 310 | fb.right_tree.event_generate('') 311 | self.window.update() 312 | self.assertEqual(fb.get_result(), (l[0],)) 313 | 314 | # --- arrow nav 315 | fb = FileBrowser(self.window, initialdir="/", mode="opendir", 316 | multiple_selection=True) 317 | self.window.update() 318 | fb.right_tree.focus_force() 319 | self.window.update() 320 | fb.event_generate('') 321 | self.window.update() 322 | fb.left_tree.focus_force() 323 | fb.event_generate('') 324 | self.window.update() 325 | fb.right_tree.focus_force() 326 | fb.event_generate('') 327 | self.window.update() 328 | fb.left_tree.focus_force() 329 | fb.event_generate('') 330 | self.window.update() 331 | fb.right_tree.focus_force() 332 | fb.event_generate('') 333 | self.window.update() 334 | fb.right_tree.focus_force() 335 | fb.event_generate('') 336 | self.window.update() 337 | fb.right_tree.focus_force() 338 | fb.event_generate('') 339 | self.window.update() 340 | fb.right_tree.focus_force() 341 | fb.event_generate('') 342 | self.window.update() 343 | fb.right_tree.focus_force() 344 | fb.event_generate('') 345 | self.window.update() 346 | 347 | def test_filebowser_foldercreation(self): 348 | initdir = os.path.abspath('/') 349 | fb = FileBrowser(self.window, initialdir=initdir, 350 | foldercreation=True) 351 | self.window.update() 352 | self.assertTrue(fb.b_new_folder.winfo_ismapped()) 353 | self.assertIs('disabled' not in fb.b_new_folder.state(), os.access(initdir, os.W_OK)) 354 | fb.display_folder(os.path.expanduser('~')) 355 | self.window.update() 356 | self.assertTrue(fb.b_new_folder.winfo_ismapped()) 357 | self.assertFalse('disabled' in fb.b_new_folder.state()) 358 | fb.left_tree.selection_clear() 359 | fb.left_tree.selection_set('recent') 360 | self.window.update() 361 | self.assertFalse(fb.b_new_folder.winfo_ismapped()) 362 | 363 | def test_filebrowser_sorting(self): 364 | fb = FileBrowser(self.window, initialdir="/", 365 | multiple_selection=True, defaultext=".png", 366 | title="Test", filetypes=[], mode="opendir", 367 | okbuttontext=None, cancelbuttontext="Cancel", 368 | foldercreation=True) 369 | self.window.update() 370 | walk = os.walk(os.path.abspath('/')) 371 | root, dirs, files = walk.send(None) 372 | dirs = [os.path.join(root, d) for d in dirs] 373 | files = [os.path.join(root, f) for f in files] 374 | 375 | # --- sort by name 376 | fb._sort_files_by_name(True) 377 | self.window.update() 378 | ch = fb.right_tree.get_children() 379 | dirs.sort(reverse=True) 380 | files.sort(reverse=True) 381 | self.assertEqual(ch, tuple(dirs) + tuple(files)) 382 | 383 | fb._sort_files_by_name(False) 384 | self.window.update() 385 | ch = fb.right_tree.get_children() 386 | dirs.sort() 387 | files.sort() 388 | self.assertEqual(ch, tuple(dirs) + tuple(files)) 389 | 390 | # --- sort by size 391 | fb._sort_by_size(False) 392 | self.window.update() 393 | ch = fb.right_tree.get_children() 394 | files.sort(key=os.path.getsize) 395 | self.assertEqual(ch, tuple(dirs) + tuple(files)) 396 | fb._sort_by_size(True) 397 | ch = fb.right_tree.get_children() 398 | files.sort(key=os.path.getsize, reverse=True) 399 | self.assertEqual(ch, tuple(dirs) + tuple(files)) 400 | 401 | # --- sort by date 402 | fb._sort_by_date(False) 403 | self.window.update() 404 | ch = fb.right_tree.get_children() 405 | dirs.sort(key=lambda d: os.path.getmtime(d)) 406 | files.sort(key=lambda f: os.path.getmtime(f)) 407 | self.assertEqual(ch, tuple(dirs) + tuple(files)) 408 | 409 | fb._sort_by_date(True) 410 | self.window.update() 411 | ch = fb.right_tree.get_children() 412 | dirs.sort(key=os.path.getmtime, reverse=True) 413 | files.sort(key=os.path.getmtime, reverse=True) 414 | self.assertEqual(ch, tuple(dirs) + tuple(files)) 415 | 416 | # --- sort by location 417 | fb.left_tree.selection_clear() 418 | fb.left_tree.selection_set('recent') 419 | self.window.update() 420 | locations = list(fb.right_tree.get_children()) 421 | fb._sort_by_location(True) 422 | self.window.update() 423 | ch = fb.right_tree.get_children() 424 | locations.sort(reverse=True) 425 | self.assertEqual(ch, tuple(locations)) 426 | 427 | fb._sort_by_location(False) 428 | self.window.update() 429 | ch = fb.right_tree.get_children() 430 | locations.sort() 431 | self.assertEqual(ch, tuple(locations)) 432 | 433 | def test_filebrowser_on_selection(self): 434 | path = os.path.expanduser('~') 435 | fb = FileBrowser(self.window, initialdir=path, mode="opendir") 436 | fb.focus_force() 437 | fb.event_generate("") 438 | self.window.update() 439 | self.assertTrue(fb.entry.winfo_ismapped()) 440 | self.assertEqual(fb.entry.get(), '') 441 | ch = fb.right_tree.tag_has('folder') 442 | if ch: 443 | fb.right_tree.selection_clear() 444 | fb.right_tree.selection_set(ch[0]) 445 | self.window.update() 446 | self.assertEqual(os.path.abspath(fb.entry.get()), 447 | os.path.abspath(os.path.join(fb.right_tree.item(ch[0], 'text'), ''))) 448 | fb.focus_force() 449 | fb.event_generate("") 450 | self.window.update() 451 | self.assertFalse(fb.entry.winfo_ismapped()) 452 | fb.focus_force() 453 | fb.event_generate("") 454 | self.window.update() 455 | ch = fb.right_tree.tag_has('file') 456 | if ch: 457 | fb.right_tree.selection_clear() 458 | fb.right_tree.selection_set(ch[0]) 459 | self.window.update() 460 | self.assertEqual(fb.entry.get(), 461 | '') 462 | fb = FileBrowser(self.window, initialdir=path, mode="openfile") 463 | fb.focus_force() 464 | fb.event_generate("") 465 | self.window.update() 466 | self.assertTrue(fb.entry.winfo_ismapped()) 467 | self.assertEqual(fb.entry.get(), '') 468 | ch = fb.right_tree.tag_has('folder') 469 | if ch: 470 | fb.right_tree.selection_clear() 471 | fb.right_tree.selection_set(ch[0]) 472 | self.window.update() 473 | self.assertEqual(os.path.abspath(fb.entry.get()), 474 | os.path.abspath(os.path.join(fb.right_tree.item(ch[0], 'text'), ''))) 475 | ch = fb.right_tree.tag_has('file') 476 | if ch: 477 | fb.right_tree.selection_clear() 478 | fb.right_tree.selection_set(ch[0]) 479 | self.window.update() 480 | self.assertEqual(fb.entry.get(), 481 | fb.right_tree.item(ch[0], 'text')) 482 | fb = FileBrowser(self.window, initialdir=path, mode="save") 483 | self.assertTrue(fb.entry.winfo_ismapped()) 484 | self.assertEqual(fb.entry.get(), '') 485 | ch = fb.right_tree.tag_has('folder') 486 | if ch: 487 | fb.right_tree.selection_clear() 488 | fb.right_tree.selection_set(ch[0]) 489 | self.window.update() 490 | self.assertEqual(fb.entry.get(), 491 | '') 492 | ch = fb.right_tree.tag_has('file') 493 | if ch: 494 | fb.right_tree.selection_clear() 495 | fb.right_tree.selection_set(ch[0]) 496 | self.window.update() 497 | self.assertEqual(fb.entry.get(), 498 | fb.right_tree.item(ch[0], 'text')) 499 | fb.left_tree.selection_clear() 500 | fb.left_tree.selection_set('recent') 501 | self.window.update() 502 | ch = fb.right_tree.tag_has('file') 503 | if ch: 504 | fb.right_tree.selection_clear() 505 | fb.right_tree.selection_set(ch[0]) 506 | self.window.update() 507 | self.assertEqual(fb.entry.get(), 508 | ch[0], 'text') 509 | -------------------------------------------------------------------------------- /tests/test_pathbutton.py: -------------------------------------------------------------------------------- 1 | from tkfilebrowser.path_button import PathButton 2 | from tests import BaseWidgetTest 3 | try: 4 | import Tkinter as tk 5 | except ImportError: 6 | import tkinter as tk 7 | 8 | 9 | class TestPathButton(BaseWidgetTest): 10 | def test_pathbutton_init(self): 11 | var = tk.StringVar(self.window) 12 | pb = PathButton(self.window, var, 'test') 13 | pb.pack() 14 | self.window.update() 15 | 16 | def test_pathbutton_methods(self): 17 | var = tk.StringVar(self.window) 18 | pb1 = PathButton(self.window, var, 'test1') 19 | pb2 = PathButton(self.window, var, 'test2') 20 | self.window.update() 21 | 22 | self.assertEqual(pb1.get_value(), 'test1') 23 | self.assertEqual(pb2.get_value(), 'test2') 24 | 25 | var.set('test1') 26 | self.assertIn('selected', pb1.state()) 27 | self.assertNotIn('selected', pb2.state()) 28 | pb2.on_press(None) 29 | self.window.update() 30 | self.assertIn('selected', pb2.state()) 31 | self.assertNotIn('selected', pb1.state()) 32 | 33 | try: 34 | n = len(var.trace_info()) 35 | except AttributeError: 36 | # fallback to old method 37 | n = len(var.trace_vinfo()) 38 | self.assertEqual(n, 2) 39 | pb2.destroy() 40 | try: 41 | n = len(var.trace_info()) 42 | except AttributeError: 43 | # fallback to old method 44 | n = len(var.trace_vinfo()) 45 | self.assertEqual(n, 1) 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/test_recentfiles.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from tkfilebrowser.recent_files import RecentFiles 4 | import unittest 5 | import tempfile 6 | import os 7 | 8 | 9 | class TestRecentFiles(unittest.TestCase): 10 | def test_recentfiles(self): 11 | filename = tempfile.mktemp() 12 | rf = RecentFiles(filename, 2) 13 | self.assertEqual(rf.get(), []) 14 | 15 | rf.add('test') 16 | self.assertEqual(rf.get(), ['test']) 17 | rf.add('test2') 18 | self.assertEqual(rf.get(), ['test2', 'test']) 19 | rf.add('test') 20 | self.assertEqual(rf.get(), ['test', 'test2']) 21 | rf.add('test3') 22 | self.assertEqual(rf.get(), ['test3', 'test']) 23 | 24 | with open(filename) as f: 25 | self.assertEqual(f.read().split(), rf.get()) 26 | 27 | del rf 28 | rf = RecentFiles(filename, 2) 29 | self.assertEqual(rf.get(), ['test3', 'test']) 30 | 31 | os.remove(filename) 32 | -------------------------------------------------------------------------------- /tests/test_tooltip.py: -------------------------------------------------------------------------------- 1 | from tkfilebrowser.tooltip import Tooltip, TooltipTreeWrapper 2 | from tests import BaseWidgetTest, TestEvent 3 | from pynput.mouse import Controller 4 | try: 5 | import ttk 6 | except ImportError: 7 | from tkinter import ttk 8 | 9 | 10 | class TestTooltip(BaseWidgetTest): 11 | def test_tooltip(self): 12 | t = Tooltip(self.window) 13 | self.window.update() 14 | t.configure(text='Hello', image=None, alpha=0.75) 15 | 16 | 17 | class TestTooltipTreeWrapper(BaseWidgetTest): 18 | def test_tooltiptreewrapper(self): 19 | tree = ttk.Treeview(self.window, show='tree') 20 | tree.pack() 21 | tree.insert("", "end", "1", text="item 1") 22 | tree.insert("", "end", "2", text="item 2") 23 | self.window.update() 24 | tw = TooltipTreeWrapper(tree) 25 | tw.add_tooltip("1", "tooltip 1") 26 | tw.add_tooltip("2", "tooltip 2") 27 | self.window.update() 28 | tw._on_motion(TestEvent(x=10, y=10)) 29 | x, y = tree.winfo_rootx(), tree.winfo_rooty() 30 | mouse_controller = Controller() 31 | mouse_controller.position = (x + 10, y + 10) 32 | tw.display_tooltip() 33 | tw._on_motion(TestEvent(x=10, y=10)) 34 | -------------------------------------------------------------------------------- /tkfilebrowser/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkfilebrowser - Alternative to filedialog for Tkinter 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkfilebrowser is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkfilebrowser is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | 21 | from tkfilebrowser.filebrowser import FileBrowser 22 | from tkfilebrowser.functions import askopendirname, askopendirnames, \ 23 | askopenfilename, askopenfilenames, askopenpathname, askopenpathnames, \ 24 | asksaveasfilename 25 | -------------------------------------------------------------------------------- /tkfilebrowser/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkfilebrowser - Alternative to filedialog for Tkinter 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkfilebrowser is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkfilebrowser is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | 20 | Example 21 | """ 22 | 23 | from tkfilebrowser import askopendirname, askopenfilenames, asksaveasfilename, askopenpathnames 24 | try: 25 | import tkinter as tk 26 | from tkinter import ttk 27 | from tkinter import filedialog 28 | except ImportError: 29 | import Tkinter as tk 30 | import ttk 31 | import tkFileDialog as filedialog 32 | 33 | root = tk.Tk() 34 | 35 | style = ttk.Style(root) 36 | style.theme_use("clam") 37 | root.configure(bg=style.lookup('TFrame', 'background')) 38 | 39 | def c_open_file_old(): 40 | rep = filedialog.askopenfilenames(parent=root, initialdir='/', initialfile='tmp', 41 | filetypes=[("PNG", "*.png"), 42 | ("JPEG", "*.jpg"), 43 | ("All files", "*")]) 44 | print(rep) 45 | 46 | 47 | def c_open_dir_old(): 48 | rep = filedialog.askdirectory(parent=root, initialdir='/tmp') 49 | print(rep) 50 | 51 | 52 | def c_save_old(): 53 | rep = filedialog.asksaveasfilename(parent=root, defaultextension=".png", 54 | initialdir='/tmp', initialfile='image.png', 55 | filetypes=[("PNG", "*.png"), 56 | ("JPEG", "*.jpg"), 57 | ("Text files", "*.txt"), 58 | ("All files", "*")]) 59 | print(rep) 60 | 61 | 62 | def c_open_file(): 63 | rep = askopenfilenames(parent=root, initialdir='/', initialfile='tmp', 64 | filetypes=[("Pictures", "*.png|*.jpg|*.JPG"), 65 | ("All files", "*")]) 66 | print(rep) 67 | 68 | 69 | def c_open_dir(): 70 | rep = askopendirname(parent=root, initialdir='/', initialfile='tmp') 71 | print(rep) 72 | 73 | 74 | def c_save(): 75 | rep = asksaveasfilename(parent=root, defaultext=".png", initialdir='/tmp', initialfile='image.png', 76 | filetypes=[("Pictures", "*.png|*.jpg|*.JPG"), 77 | ("Text files", "*.txt"), 78 | ("All files", "*")]) 79 | print(rep) 80 | 81 | 82 | def c_path(): 83 | rep = askopenpathnames(parent=root, initialdir='/', initialfile='tmp') 84 | print(rep) 85 | 86 | 87 | ttk.Label(root, text='Default dialogs').grid(row=0, column=0, padx=4, pady=4, sticky='ew') 88 | ttk.Label(root, text='tkfilebrowser dialogs').grid(row=0, column=1, padx=4, pady=4, sticky='ew') 89 | ttk.Button(root, text="Open files", command=c_open_file_old).grid(row=1, column=0, padx=4, pady=4, sticky='ew') 90 | ttk.Button(root, text="Open folder", command=c_open_dir_old).grid(row=2, column=0, padx=4, pady=4, sticky='ew') 91 | ttk.Button(root, text="Save file", command=c_save_old).grid(row=3, column=0, padx=4, pady=4, sticky='ew') 92 | ttk.Button(root, text="Open files", command=c_open_file).grid(row=1, column=1, padx=4, pady=4, sticky='ew') 93 | ttk.Button(root, text="Open folder", command=c_open_dir).grid(row=2, column=1, padx=4, pady=4, sticky='ew') 94 | ttk.Button(root, text="Save file", command=c_save).grid(row=3, column=1, padx=4, pady=4, sticky='ew') 95 | ttk.Button(root, text="Open paths", command=c_path).grid(row=4, column=1, padx=4, pady=4, sticky='ew') 96 | 97 | root.mainloop() 98 | -------------------------------------------------------------------------------- /tkfilebrowser/autoscrollbar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkfilebrowser - Alternative to filedialog for Tkinter 4 | Copyright 2017 Juliette Monsel 5 | based on code by Fredrik Lundh copyright 1998 6 | 7 | 8 | tkfilebrowser is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | tkfilebrowser is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | 22 | Scrollbar that hides automatically when not needed 23 | """ 24 | 25 | 26 | from tkfilebrowser.constants import tk, ttk 27 | 28 | 29 | class AutoScrollbar(ttk.Scrollbar): 30 | """Scrollbar that hides itself if it's not needed.""" 31 | 32 | def set(self, lo, hi): 33 | if float(lo) <= 0.0 and float(hi) >= 1.0: 34 | self.grid_remove() 35 | else: 36 | self.grid() 37 | ttk.Scrollbar.set(self, lo, hi) 38 | 39 | def pack(self, **kw): 40 | raise tk.TclError("cannot use pack with this widget") 41 | 42 | def place(self, **kw): 43 | raise tk.TclError("cannot use place with this widget") 44 | -------------------------------------------------------------------------------- /tkfilebrowser/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkfilebrowser - Alternative to filedialog for Tkinter 4 | Copyright 2017-2018 Juliette Monsel 5 | 6 | tkfilebrowser is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkfilebrowser is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | 20 | The icons are modified versions of icons from the elementary project 21 | (the xfce fork to be precise https://github.com/shimmerproject/elementary-xfce) 22 | Copyright 2007-2013 elementary LLC. 23 | 24 | 25 | Constants and functions 26 | """ 27 | import locale 28 | from babel.numbers import format_number 29 | from babel.dates import format_date, format_datetime 30 | from datetime import datetime 31 | import os 32 | from math import log, floor 33 | 34 | try: 35 | import tkinter as tk 36 | from tkinter import ttk 37 | from tkinter.messagebox import askyesnocancel, showerror 38 | from urllib.parse import unquote 39 | except ImportError: 40 | import Tkinter as tk 41 | import ttk 42 | from tkMessageBox import askyesnocancel, showerror 43 | from urllib import unquote 44 | import sys 45 | reload(sys) 46 | sys.setdefaultencoding('utf8') 47 | 48 | PATH = os.path.dirname(__file__) 49 | 50 | LOCAL_PATH = os.path.join(os.path.expanduser('~'), '.config', 'tkfilebrowser') 51 | 52 | if not os.path.exists(LOCAL_PATH): 53 | try: 54 | if not os.path.exists(os.path.join(os.path.expanduser('~'), '.config')): 55 | os.mkdir(os.path.join(os.path.expanduser('~'), '.config')) 56 | os.mkdir(LOCAL_PATH) 57 | except Exception: 58 | # avoid raising error if the path is not writtable 59 | pass 60 | 61 | RECENT_FILES = os.path.join(LOCAL_PATH, 'recent_files') 62 | 63 | # --- images 64 | if tk.TkVersion < 8.6: 65 | from PIL.ImageTk import PhotoImage 66 | else: 67 | PhotoImage = tk.PhotoImage 68 | 69 | IM_HOME = os.path.join(PATH, "images", "home.png") 70 | IM_DESKTOP = os.path.join(PATH, "images", "desktop.png") 71 | IM_FOLDER = os.path.join(PATH, "images", "folder.png") 72 | IM_FOLDER_LINK = os.path.join(PATH, "images", "folder_link.png") 73 | IM_NEW = os.path.join(PATH, "images", "new_folder.png") 74 | IM_FILE = os.path.join(PATH, "images", "file.png") 75 | IM_FILE_LINK = os.path.join(PATH, "images", "file_link.png") 76 | IM_LINK_BROKEN = os.path.join(PATH, "images", "link_broken.png") 77 | IM_DRIVE = os.path.join(PATH, "images", "drive.png") 78 | IM_RECENT = os.path.join(PATH, "images", "recent.png") 79 | IM_RECENT_24 = os.path.join(PATH, "images", "recent_24.png") 80 | 81 | # --- translation 82 | try: 83 | LANG = locale.getdefaultlocale()[0] 84 | except ValueError: 85 | LANG = 'en' 86 | 87 | EN = {} 88 | FR = {"B": "octets", "MB": "Mo", "kB": "ko", "GB": "Go", "TB": "To", 89 | "Name: ": "Nom : ", "Folder: ": "Dossier : ", "Size": "Taille", 90 | "Name": "Nom", "Modified": "Modifié", "Save": "Enregistrer", 91 | "Open": "Ouvrir", "Cancel": "Annuler", "Location": "Emplacement", 92 | "Today": "Aujourd'hui", "Confirmation": "Confirmation", 93 | "Error": "Erreur", 94 | "The file {file} already exists, do you want to replace it?": "Le fichier {file} existe déjà, voulez-vous le remplacer ?", 95 | "Shortcuts": "Raccourcis", "Save As": "Enregistrer sous", 96 | "Recent": "Récents", "Recently used": "Récemment utilisés"} 97 | LANGUAGES = {"fr": FR, "en": EN} 98 | if LANG[:2] == "fr": 99 | TR = LANGUAGES["fr"] 100 | else: 101 | TR = LANGUAGES["en"] 102 | 103 | 104 | def _(text): 105 | """ translation function """ 106 | return TR.get(text, text) 107 | 108 | 109 | fromtimestamp = datetime.fromtimestamp 110 | 111 | 112 | def locale_date(date=None): 113 | return format_date(date, 'short', locale=LANG) 114 | 115 | 116 | def locale_datetime(date=None): 117 | return format_datetime(date, 'EEEE HH:mm', locale=LANG) 118 | 119 | 120 | def locale_number(nb): 121 | return format_number(nb, locale=LANG) 122 | 123 | 124 | SIZES = [_("B"), _("kB"), _("MB"), _("GB"), _("TB")] 125 | 126 | # --- locale settings for dates 127 | TODAY = locale_date() 128 | YEAR = datetime.now().year 129 | DAY = int(format_date(None, 'D', locale=LANG)) 130 | 131 | 132 | # --- functions 133 | def add_trace(variable, mode, callback): 134 | """ 135 | Add trace to variable. 136 | 137 | Ensure compatibility with old and new trace method. 138 | mode: "read", "write", "unset" (new syntax) 139 | """ 140 | try: 141 | return variable.trace_add(mode, callback) 142 | except AttributeError: 143 | # fallback to old method 144 | return variable.trace(mode[0], callback) 145 | 146 | 147 | def remove_trace(variable, mode, cbname): 148 | """ 149 | Remove trace from variable. 150 | 151 | Ensure compatibility with old and new trace method. 152 | mode: "read", "write", "unset" (new syntax) 153 | """ 154 | try: 155 | variable.trace_remove(mode, cbname) 156 | except AttributeError: 157 | # fallback to old method 158 | variable.trace_vdelete(mode[0], cbname) 159 | 160 | 161 | def get_modification_date(file): 162 | """Return the modification date of file.""" 163 | try: 164 | tps = fromtimestamp(os.path.getmtime(file)) 165 | except OSError: 166 | tps = TODAY 167 | date = locale_date(tps) 168 | if date == TODAY: 169 | date = _("Today") + tps.strftime(" %H:%M") 170 | elif tps.year == YEAR and (DAY - int(tps.strftime("%j"))) < 7: 171 | date = locale_datetime(tps) 172 | return date 173 | 174 | 175 | def display_modification_date(mtime): 176 | """Return the modDification date of file.""" 177 | if isinstance(mtime, str): 178 | return mtime 179 | tps = fromtimestamp(mtime) 180 | date = locale_date(tps) 181 | if date == TODAY: 182 | date = _("Today") + tps.strftime(" %H:%M") 183 | elif tps.year == YEAR and (DAY - int(tps.strftime("%j"))) < 7: 184 | date = locale_datetime(tps) 185 | return date 186 | 187 | 188 | def display_size(size_o): 189 | """Return the size of file.""" 190 | if isinstance(size_o, str): 191 | return size_o 192 | if size_o > 0: 193 | m = int(floor(log(size_o) / log(1024))) 194 | if m < len(SIZES): 195 | unit = SIZES[m] 196 | s = size_o / (1024 ** m) 197 | else: 198 | unit = SIZES[-1] 199 | s = size_o / (1024**(len(SIZES) - 1)) 200 | size = "%s %s" % (locale_number("%.1f" % s), unit) 201 | else: 202 | size = "0 " + _("B") 203 | return size 204 | 205 | 206 | def key_sort_files(file): 207 | return file.is_file(), file.name.lower() 208 | -------------------------------------------------------------------------------- /tkfilebrowser/filebrowser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkfilebrowser - Alternative to filedialog for Tkinter 4 | Copyright 2017-2018 Juliette Monsel 5 | 6 | tkfilebrowser is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkfilebrowser is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | 20 | Main class 21 | """ 22 | 23 | 24 | import psutil 25 | from re import search 26 | from subprocess import check_output 27 | from os import walk, mkdir, stat, access, W_OK, listdir 28 | from os import name as OSNAME 29 | from os.path import sep as SEP 30 | from os.path import exists, join, getmtime, realpath, split, expanduser, \ 31 | abspath, isabs, splitext, dirname, getsize, isdir, isfile, islink 32 | try: 33 | from os import scandir 34 | SCANDIR = True 35 | except ImportError: 36 | SCANDIR = False 37 | import traceback 38 | import tkfilebrowser.constants as cst 39 | from tkfilebrowser.constants import unquote, tk, ttk, key_sort_files, \ 40 | get_modification_date, display_modification_date, display_size 41 | from tkfilebrowser.autoscrollbar import AutoScrollbar 42 | from tkfilebrowser.path_button import PathButton 43 | from tkfilebrowser.tooltip import TooltipTreeWrapper 44 | from tkfilebrowser.recent_files import RecentFiles 45 | 46 | if OSNAME == 'nt': 47 | from win32com.shell import shell, shellcon 48 | 49 | _ = cst._ 50 | 51 | 52 | class Stats: 53 | """Fake stats class to create dummy stats for broken links.""" 54 | def __init__(self, **kwargs): 55 | self._prop = kwargs 56 | 57 | def __getattr__(self, attr): 58 | if attr not in self._prop: 59 | raise AttributeError("Stats has no attribute %s." % attr) 60 | else: 61 | return self._prop[attr] 62 | 63 | 64 | class FileBrowser(tk.Toplevel): 65 | """Filebrowser dialog class.""" 66 | def __init__(self, parent, initialdir="", initialfile="", mode="openfile", 67 | multiple_selection=False, defaultext="", title="Filebrowser", 68 | filetypes=[], okbuttontext=None, cancelbuttontext=_("Cancel"), 69 | foldercreation=True, **kw): 70 | """ 71 | Create a filebrowser dialog. 72 | 73 | Arguments: 74 | 75 | parent : Tk or Toplevel instance 76 | parent window 77 | 78 | title : str 79 | the title of the filebrowser window 80 | 81 | initialdir : str 82 | directory whose content is initially displayed 83 | 84 | initialfile : str 85 | initially selected item (just the name, not the full path) 86 | 87 | mode : str 88 | kind of dialog: "openpath", "openfile", "opendir" or "save" 89 | 90 | multiple_selection : bool 91 | whether to allow multiple items selection (open modes only) 92 | 93 | defaultext : str (e.g. '.png') 94 | extension added to filename if none is given (default is none) 95 | 96 | filetypes : list :obj:`[("name", "*.ext1|*.ext2|.."), ...]` 97 | only the files of given filetype will be displayed, 98 | e.g. to allow the user to switch between displaying only PNG or JPG 99 | pictures or dispalying all files: 100 | :obj:`filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")]` 101 | 102 | okbuttontext : str 103 | text displayed on the validate button, default is "Open". 104 | 105 | cancelbuttontext : str 106 | text displayed on the button that cancels the selection, default is "Cancel". 107 | 108 | foldercreation : bool 109 | enable the user to create new folders if True (default) 110 | """ 111 | # compatibility with tkinter.filedialog arguments: the parent window is called 'master' 112 | if 'master' in kw and parent is None: 113 | parent = kw.pop('master') 114 | if 'defaultextension' in kw and not defaultext: 115 | defaultext = kw.pop('defaultextension') 116 | tk.Toplevel.__init__(self, parent, **kw) 117 | 118 | # python version compatibility 119 | if SCANDIR: 120 | self.display_folder = self._display_folder_scandir 121 | else: 122 | self.display_folder = self._display_folder_walk 123 | 124 | # keep track of folders to be able to move backward/foreward in history 125 | if initialdir: 126 | self.history = [initialdir] 127 | else: 128 | self.history = [expanduser("~")] 129 | self._hist_index = -1 130 | 131 | self.transient(parent) 132 | self.grab_set() 133 | self.protocol("WM_DELETE_WINDOW", self.quit) 134 | self.title(title) 135 | 136 | self.rowconfigure(2, weight=1) 137 | self.columnconfigure(0, weight=1) 138 | 139 | self.mode = mode 140 | self.result = "" 141 | self.foldercreation = foldercreation 142 | 143 | # hidden files/folders visibility 144 | self.hide = False 145 | # hidden items 146 | self.hidden = () 147 | 148 | # --- style 149 | style = ttk.Style(self) 150 | bg = style.lookup("TFrame", "background") 151 | style.layout("right.tkfilebrowser.Treeview.Item", 152 | [('Treeitem.padding', 153 | {'children': 154 | [('Treeitem.image', {'side': 'left', 'sticky': ''}), 155 | ('Treeitem.focus', 156 | {'children': 157 | [('Treeitem.text', 158 | {'side': 'left', 'sticky': ''})], 159 | 'side': 'left', 160 | 'sticky': ''})], 161 | 'sticky': 'nswe'})]) 162 | style.layout("left.tkfilebrowser.Treeview.Item", 163 | [('Treeitem.padding', 164 | {'children': 165 | [('Treeitem.image', {'side': 'left', 'sticky': ''}), 166 | ('Treeitem.focus', 167 | {'children': 168 | [('Treeitem.text', {'side': 'left', 'sticky': ''})], 169 | 'side': 'left', 170 | 'sticky': ''})], 171 | 'sticky': 'nswe'})]) 172 | style.configure("right.tkfilebrowser.Treeview", font="TkDefaultFont") 173 | style.configure("right.tkfilebrowser.Treeview.Item", padding=2) 174 | style.configure("right.tkfilebrowser.Treeview.Heading", 175 | font="TkDefaultFont") 176 | style.configure("left.tkfilebrowser.Treeview.Heading", 177 | font="TkDefaultFont") 178 | style.configure("left.tkfilebrowser.Treeview.Item", padding=2) 179 | style.configure("listbox.tkfilebrowser.TFrame", background="white", relief="sunken") 180 | field_bg = style.lookup("TEntry", "fieldbackground", default='white') 181 | tree_field_bg = style.lookup("ttk.Treeview", "fieldbackground", 182 | default='white') 183 | fg = style.lookup('TLabel', 'foreground', default='black') 184 | active_bg = style.lookup('TButton', 'background', ('active',)) 185 | sel_bg = style.lookup('Treeview', 'background', ('selected',)) 186 | sel_fg = style.lookup('Treeview', 'foreground', ('selected',)) 187 | self.option_add('*TCombobox*Listbox.selectBackground', sel_bg) 188 | self.option_add('*TCombobox*Listbox.selectForeground', sel_fg) 189 | style.map('types.tkfilebrowser.TCombobox', foreground=[], fieldbackground=[]) 190 | style.configure('types.tkfilebrowser.TCombobox', lightcolor=bg, 191 | fieldbackground=bg) 192 | style.configure('types.tkfilebrowser.TCombobox.Item', background='red') 193 | style.configure("left.tkfilebrowser.Treeview", background=active_bg, 194 | font="TkDefaultFont", 195 | fieldbackground=active_bg) 196 | self.configure(background=bg) 197 | # path button style 198 | style.configure("path.tkfilebrowser.TButton", padding=2) 199 | selected_bg = style.lookup("TButton", "background", ("pressed",)) 200 | map_bg = style.map("TButton", "background") 201 | map_bg.append(("selected", selected_bg)) 202 | style.map("path.tkfilebrowser.TButton", 203 | background=map_bg, 204 | font=[("selected", "TkDefaultFont 9 bold")]) 205 | # tooltip style 206 | style.configure('tooltip.tkfilebrowser.TLabel', background='black', 207 | foreground='white') 208 | 209 | # --- images 210 | self.im_file = cst.PhotoImage(file=cst.IM_FILE, master=self) 211 | self.im_folder = cst.PhotoImage(file=cst.IM_FOLDER, master=self) 212 | self.im_desktop = cst.PhotoImage(file=cst.IM_DESKTOP, master=self) 213 | self.im_file_link = cst.PhotoImage(file=cst.IM_FILE_LINK, master=self) 214 | self.im_link_broken = cst.PhotoImage(file=cst.IM_LINK_BROKEN, master=self) 215 | self.im_folder_link = cst.PhotoImage(file=cst.IM_FOLDER_LINK, master=self) 216 | self.im_new = cst.PhotoImage(file=cst.IM_NEW, master=self) 217 | self.im_drive = cst.PhotoImage(file=cst.IM_DRIVE, master=self) 218 | self.im_home = cst.PhotoImage(file=cst.IM_HOME, master=self) 219 | self.im_recent = cst.PhotoImage(file=cst.IM_RECENT, master=self) 220 | self.im_recent_24 = cst.PhotoImage(file=cst.IM_RECENT_24, master=self) 221 | 222 | # --- filetypes 223 | self.filetype = tk.StringVar(self) 224 | self.filetypes = {} 225 | if filetypes: 226 | for name, exts in filetypes: 227 | if name not in self.filetypes: 228 | self.filetypes[name] = [] 229 | self.filetypes[name] = r'%s$' % exts.strip().replace('.', '\.').replace('*', '.*') 230 | values = list(self.filetypes.keys()) 231 | w = max([len(f) for f in values] + [5]) 232 | b_filetype = ttk.Combobox(self, textvariable=self.filetype, 233 | state='readonly', 234 | style='types.tkfilebrowser.TCombobox', 235 | values=values, 236 | width=w) 237 | b_filetype.grid(row=3, sticky="e", padx=10, pady=(4, 0)) 238 | self.filetype.set(filetypes[0][0]) 239 | try: 240 | self.filetype.trace_add('write', lambda *args: self._change_filetype()) 241 | except AttributeError: 242 | self.filetype.trace('w', lambda *args: self._change_filetype()) 243 | else: 244 | self.filetypes[""] = r".*$" 245 | 246 | # --- recent files 247 | self._recent_files = RecentFiles(cst.RECENT_FILES, 30) 248 | 249 | # --- path completion 250 | self.complete = self.register(self._completion) 251 | self.listbox_var = tk.StringVar(self) 252 | self.listbox_frame = ttk.Frame(self, style="listbox.tkfilebrowser.TFrame", borderwidth=1) 253 | self.listbox = tk.Listbox(self.listbox_frame, 254 | listvariable=self.listbox_var, 255 | highlightthickness=0, 256 | borderwidth=0, 257 | background=field_bg, 258 | foreground=fg, 259 | selectforeground=sel_fg, 260 | selectbackground=sel_bg) 261 | self.listbox.pack(expand=True, fill="x") 262 | 263 | # --- path bar 264 | self.path_var = tk.StringVar(self) 265 | frame_bar = ttk.Frame(self) 266 | frame_bar.columnconfigure(0, weight=1) 267 | frame_bar.grid(row=1, sticky="ew", pady=10, padx=10) 268 | frame_recent = ttk.Frame(frame_bar) 269 | frame_recent.grid(row=0, column=0, sticky="w") 270 | ttk.Label(frame_recent, image=self.im_recent_24).pack(side="left") 271 | ttk.Label(frame_recent, text=_("Recently used"), 272 | font="TkDefaultFont 9 bold").pack(side="left", padx=4) 273 | self.path_bar = ttk.Frame(frame_bar) 274 | self.path_bar.grid(row=0, column=0, sticky="ew") 275 | self.path_bar_buttons = [] 276 | self.b_new_folder = ttk.Button(frame_bar, image=self.im_new, 277 | command=self.create_folder) 278 | if self.foldercreation: 279 | self.b_new_folder.grid(row=0, column=1, sticky="e") 280 | if mode == "save": 281 | ttk.Label(self.path_bar, text=_("Folder: ")).grid(row=0, column=0) 282 | self.defaultext = defaultext 283 | 284 | frame_name = ttk.Frame(self) 285 | frame_name.grid(row=0, pady=(10, 0), padx=10, sticky="ew") 286 | ttk.Label(frame_name, text=_("Name: ")).pack(side="left") 287 | self.entry = ttk.Entry(frame_name, validate="key", 288 | validatecommand=(self.complete, "%d", "%S", 289 | "%i", "%s")) 290 | self.entry.pack(side="left", fill="x", expand=True) 291 | 292 | if initialfile: 293 | self.entry.insert(0, initialfile) 294 | else: 295 | self.multiple_selection = multiple_selection 296 | self.entry = ttk.Entry(frame_bar, validate="key", 297 | validatecommand=(self.complete, "%d", "%S", 298 | "%i", "%s")) 299 | self.entry.grid(row=1, column=0, columnspan=2, sticky="ew", padx=0, 300 | pady=(10, 0)) 301 | self.entry.grid_remove() 302 | 303 | paned = ttk.PanedWindow(self, orient="horizontal") 304 | paned.grid(row=2, sticky="eswn", padx=10) 305 | 306 | # --- left pane 307 | left_pane = ttk.Frame(paned) 308 | left_pane.columnconfigure(0, weight=1) 309 | left_pane.rowconfigure(0, weight=1) 310 | 311 | paned.add(left_pane, weight=0) 312 | self.left_tree = ttk.Treeview(left_pane, selectmode="browse", 313 | style="left.tkfilebrowser.Treeview") 314 | wrapper = TooltipTreeWrapper(self.left_tree) 315 | self.left_tree.column("#0", width=150) 316 | self.left_tree.heading("#0", text=_("Shortcuts"), anchor="w") 317 | self.left_tree.grid(row=0, column=0, sticky="sewn") 318 | 319 | scroll_left = AutoScrollbar(left_pane, command=self.left_tree.yview) 320 | scroll_left.grid(row=0, column=1, sticky="ns") 321 | self.left_tree.configure(yscrollcommand=scroll_left.set) 322 | 323 | # list devices and bookmarked locations 324 | # -------- recent 325 | self.left_tree.insert("", "end", iid="recent", text=_("Recent"), 326 | image=self.im_recent) 327 | wrapper.add_tooltip("recent", _("Recently used")) 328 | 329 | # -------- devices 330 | devices = psutil.disk_partitions(all=True if OSNAME == "nt" else False) 331 | 332 | for d in devices: 333 | m = d.mountpoint 334 | if m == "/": 335 | txt = "/" 336 | else: 337 | if OSNAME == 'nt': 338 | txt = m 339 | else: 340 | txt = split(m)[-1] 341 | self.left_tree.insert("", "end", iid=m, text=txt, 342 | image=self.im_drive) 343 | wrapper.add_tooltip(m, m) 344 | 345 | # -------- home 346 | home = expanduser("~") 347 | self.left_tree.insert("", "end", iid=home, image=self.im_home, 348 | text=split(home)[-1]) 349 | wrapper.add_tooltip(home, home) 350 | 351 | # -------- desktop 352 | if OSNAME == 'nt': 353 | desktop = shell.SHGetFolderPath(0, shellcon.CSIDL_DESKTOP, None, 0) 354 | else: 355 | try: 356 | desktop = check_output(['xdg-user-dir', 'DESKTOP']).decode().strip() 357 | except Exception: 358 | # FileNotFoundError in python3 if xdg-users-dir is not installed, 359 | # but OSError in python2 360 | desktop = join(home, 'Desktop') 361 | if exists(desktop): 362 | self.left_tree.insert("", "end", iid=desktop, image=self.im_desktop, 363 | text=split(desktop)[-1]) 364 | wrapper.add_tooltip(desktop, desktop) 365 | 366 | # -------- bookmarks 367 | if OSNAME == 'nt': 368 | bm = [] 369 | for folder in [shellcon.CSIDL_PERSONAL, shellcon.CSIDL_MYPICTURES, 370 | shellcon.CSIDL_MYMUSIC, shellcon.CSIDL_MYVIDEO]: 371 | try: 372 | bm.append([shell.SHGetFolderPath(0, folder, None, 0)]) 373 | except Exception: 374 | pass 375 | else: 376 | path_bm = join(home, ".config", "gtk-3.0", "bookmarks") 377 | path_bm2 = join(home, ".gtk-bookmarks") # old location 378 | if exists(path_bm): 379 | with open(path_bm) as f: 380 | bms = f.read().splitlines() 381 | elif exists(path_bm2): 382 | with open(path_bm) as f: 383 | bms = f.read().splitlines() 384 | else: 385 | bms = [] 386 | bms = [ch.split() for ch in bms] 387 | bm = [] 388 | for ch in bms: 389 | ch[0] = unquote(ch[0]).replace("file://", "") 390 | bm.append(ch) 391 | for l in bm: 392 | if len(l) == 1: 393 | txt = split(l[0])[-1] 394 | else: 395 | txt = l[1] 396 | self.left_tree.insert("", "end", iid=l[0], 397 | text=txt, 398 | image=self.im_folder) 399 | wrapper.add_tooltip(l[0], l[0]) 400 | 401 | # --- right pane 402 | right_pane = ttk.Frame(paned) 403 | right_pane.columnconfigure(0, weight=1) 404 | right_pane.rowconfigure(0, weight=1) 405 | paned.add(right_pane, weight=1) 406 | 407 | if mode != "save" and multiple_selection: 408 | selectmode = "extended" 409 | else: 410 | selectmode = "browse" 411 | 412 | self.right_tree = ttk.Treeview(right_pane, selectmode=selectmode, 413 | style="right.tkfilebrowser.Treeview", 414 | columns=("location", "size", "date"), 415 | displaycolumns=("size", "date")) 416 | # headings 417 | self.right_tree.heading("#0", text=_("Name"), anchor="w", 418 | command=lambda: self._sort_files_by_name(True)) 419 | self.right_tree.heading("location", text=_("Location"), anchor="w", 420 | command=lambda: self._sort_by_location(False)) 421 | self.right_tree.heading("size", text=_("Size"), anchor="w", 422 | command=lambda: self._sort_by_size(False)) 423 | self.right_tree.heading("date", text=_("Modified"), anchor="w", 424 | command=lambda: self._sort_by_date(False)) 425 | # columns 426 | self.right_tree.column("#0", width=250) 427 | self.right_tree.column("location", width=100) 428 | self.right_tree.column("size", stretch=False, width=85) 429 | self.right_tree.column("date", width=120) 430 | # tags 431 | self.right_tree.tag_configure("0", background=tree_field_bg) 432 | self.right_tree.tag_configure("1", background=active_bg) 433 | self.right_tree.tag_configure("folder", image=self.im_folder) 434 | self.right_tree.tag_configure("file", image=self.im_file) 435 | self.right_tree.tag_configure("folder_link", image=self.im_folder_link) 436 | self.right_tree.tag_configure("file_link", image=self.im_file_link) 437 | self.right_tree.tag_configure("link_broken", image=self.im_link_broken) 438 | if mode == "opendir": 439 | self.right_tree.tag_configure("file", foreground="gray") 440 | self.right_tree.tag_configure("file_link", foreground="gray") 441 | 442 | self.right_tree.grid(row=0, column=0, sticky="eswn") 443 | # scrollbar 444 | self._scroll_h = AutoScrollbar(right_pane, orient='horizontal', 445 | command=self.right_tree.xview) 446 | self._scroll_h.grid(row=1, column=0, sticky='ew') 447 | scroll_right = AutoScrollbar(right_pane, command=self.right_tree.yview) 448 | scroll_right.grid(row=0, column=1, sticky="ns") 449 | self.right_tree.configure(yscrollcommand=scroll_right.set, 450 | xscrollcommand=self._scroll_h.set) 451 | 452 | # --- buttons 453 | frame_buttons = ttk.Frame(self) 454 | frame_buttons.grid(row=4, sticky="ew", pady=10, padx=10) 455 | if okbuttontext is None: 456 | if mode == "save": 457 | okbuttontext = _("Save") 458 | else: 459 | okbuttontext = _("Open") 460 | ttk.Button(frame_buttons, text=okbuttontext, 461 | command=self.validate).pack(side="right") 462 | ttk.Button(frame_buttons, text=cancelbuttontext, 463 | command=self.quit).pack(side="right", padx=4) 464 | 465 | # --- key browsing entry 466 | self.key_browse_var = tk.StringVar(self) 467 | self.key_browse_entry = ttk.Entry(self, textvariable=self.key_browse_var, 468 | width=10) 469 | cst.add_trace(self.key_browse_var, "write", self._key_browse) 470 | # list of folders/files beginning by the letters inserted in self.key_browse_entry 471 | self.paths_beginning_by = [] 472 | self.paths_beginning_by_index = 0 # current index in the list 473 | 474 | # --- initialization 475 | if not initialdir: 476 | initialdir = expanduser("~") 477 | 478 | self.display_folder(initialdir) 479 | initialpath = join(initialdir, initialfile) 480 | if initialpath in self.right_tree.get_children(""): 481 | self.right_tree.see(initialpath) 482 | self.right_tree.selection_add(initialpath) 483 | 484 | # --- bindings 485 | # filetype combobox 486 | self.bind_class('TCombobox', '<>', 487 | lambda e: e.widget.selection_clear(), 488 | add=True) 489 | # left tree 490 | self.left_tree.bind("<>", self._shortcut_select) 491 | # right tree 492 | self.right_tree.bind("", self._select) 493 | self.right_tree.bind("", self._select) 494 | self.right_tree.bind("", self._go_left) 495 | if multiple_selection: 496 | self.right_tree.bind("", self._right_tree_select_all) 497 | 498 | if mode == "save": 499 | self.right_tree.bind("<>", 500 | self._file_selection_save) 501 | elif mode == "opendir": 502 | self.right_tree.bind("<>", 503 | self._file_selection_opendir) 504 | else: 505 | self.right_tree.bind("<>", 506 | self._file_selection_openfile) 507 | 508 | self.right_tree.bind("", self._key_browse_show) 509 | # listbox 510 | self.listbox.bind("", 511 | lambda e: self.listbox_frame.place_forget()) 512 | # path entry 513 | self.entry.bind("", 514 | lambda e: self.listbox_frame.place_forget()) 515 | self.entry.bind("", self._down) 516 | self.entry.bind("", self.validate) 517 | self.entry.bind("", self._tab) 518 | self.entry.bind("", self._tab) 519 | self.entry.bind("", self._select_all) 520 | 521 | # key browse entry 522 | self.key_browse_entry.bind("", self._key_browse_hide) 523 | self.key_browse_entry.bind("", self._key_browse_hide) 524 | self.key_browse_entry.bind("", self._key_browse_validate) 525 | 526 | # main bindings 527 | self.bind("", self.toggle_hidden) 528 | self.bind("", self._hist_backward) 529 | self.bind("", self._hist_forward) 530 | self.bind("", self._go_to_parent) 531 | self.bind("", self._go_to_child) 532 | self.bind("", self._unpost, add=True) 533 | self.bind("", self._hide_listbox) 534 | 535 | if mode != "save": 536 | self.bind("", self.toggle_path_entry) 537 | if self.foldercreation: 538 | self.right_tree.bind("", self.create_folder) 539 | 540 | self.update_idletasks() 541 | self.lift() 542 | if mode == 'save': 543 | self.entry.selection_range(0, 'end') 544 | self.entry.focus_set() 545 | 546 | def _right_tree_select_all(self, event): 547 | if self.mode == "openpath": 548 | items = self.right_tree.tag_has('folder') + self.right_tree.tag_has('folder_link') \ 549 | + self.right_tree.tag_has('file') + self.right_tree.tag_has('file_link') 550 | elif self.mode == 'opendir': 551 | items = self.right_tree.tag_has('folder') + self.right_tree.tag_has('folder_link') 552 | else: 553 | items = self.right_tree.tag_has('file') + self.right_tree.tag_has('file_link') 554 | self.right_tree.selection_clear() 555 | self.right_tree.selection_set(items) 556 | 557 | def _select_all(self, event): 558 | """Select all entry content.""" 559 | event.widget.selection_range(0, "end") 560 | return "break" # suppress class binding 561 | 562 | # --- key browsing 563 | def _key_browse_hide(self, event): 564 | """Hide key browsing entry.""" 565 | if self.key_browse_entry.winfo_ismapped(): 566 | self.key_browse_entry.place_forget() 567 | self.key_browse_entry.delete(0, "end") 568 | 569 | def _key_browse_show(self, event): 570 | """Show key browsing entry.""" 571 | if event.char.isalnum() or event.char in [".", "_", "(", "-", "*", "$"]: 572 | self.key_browse_entry.place(in_=self.right_tree, relx=0, rely=1, 573 | y=4, x=1, anchor="nw") 574 | self.key_browse_entry.focus_set() 575 | self.key_browse_entry.insert(0, event.char) 576 | 577 | def _key_browse_validate(self, event): 578 | """Hide key browsing entry and validate selection.""" 579 | self._key_browse_hide(event) 580 | self.right_tree.focus_set() 581 | self.validate() 582 | 583 | def _key_browse(self, *args): 584 | """Use keyboard to browse tree.""" 585 | self.key_browse_entry.unbind("") 586 | self.key_browse_entry.unbind("") 587 | deb = self.key_browse_entry.get().lower() 588 | if deb: 589 | if self.mode == 'opendir': 590 | children = list(self.right_tree.tag_has("folder")) 591 | children.extend(self.right_tree.tag_has("folder_link")) 592 | children.sort() 593 | else: 594 | children = self.right_tree.get_children("") 595 | self.paths_beginning_by = [i for i in children if split(i)[-1][:len(deb)].lower() == deb] 596 | sel = self.right_tree.selection() 597 | if sel: 598 | self.right_tree.selection_remove(*sel) 599 | if self.paths_beginning_by: 600 | self.paths_beginning_by_index = 0 601 | self._browse_list(0) 602 | self.key_browse_entry.bind("", 603 | lambda e: self._browse_list(-1)) 604 | self.key_browse_entry.bind("", 605 | lambda e: self._browse_list(1)) 606 | 607 | def _browse_list(self, delta): 608 | """ 609 | Navigate between folders/files with Up/Down keys. 610 | 611 | Navigation between folders/files beginning by the letters in 612 | self.key_browse_entry. 613 | """ 614 | self.paths_beginning_by_index += delta 615 | self.paths_beginning_by_index %= len(self.paths_beginning_by) 616 | sel = self.right_tree.selection() 617 | if sel: 618 | self.right_tree.selection_remove(*sel) 619 | path = abspath(join(self.history[self._hist_index], 620 | self.paths_beginning_by[self.paths_beginning_by_index])) 621 | self.right_tree.see(path) 622 | self.right_tree.selection_add(path) 623 | 624 | # --- column sorting 625 | def _sort_files_by_name(self, reverse): 626 | """Sort files and folders by (reversed) alphabetical order.""" 627 | files = list(self.right_tree.tag_has("file")) 628 | files.extend(list(self.right_tree.tag_has("file_link"))) 629 | folders = list(self.right_tree.tag_has("folder")) 630 | folders.extend(list(self.right_tree.tag_has("folder_link"))) 631 | files.sort(reverse=reverse) 632 | folders.sort(reverse=reverse) 633 | 634 | for index, item in enumerate(folders): 635 | self.move_item(item, index) 636 | l = len(folders) 637 | 638 | for index, item in enumerate(files): 639 | self.move_item(item, index + l) 640 | self.right_tree.heading("#0", 641 | command=lambda: self._sort_files_by_name(not reverse)) 642 | 643 | def _sort_by_location(self, reverse): 644 | """Sort files by location.""" 645 | l = [(self.right_tree.set(k, "location"), k) for k in self.right_tree.get_children('')] 646 | l.sort(reverse=reverse) 647 | for index, (val, k) in enumerate(l): 648 | self.move_item(k, index) 649 | self.right_tree.heading("location", 650 | command=lambda: self._sort_by_location(not reverse)) 651 | 652 | def _sort_by_size(self, reverse): 653 | """Sort files by size.""" 654 | files = list(self.right_tree.tag_has("file")) 655 | files.extend(list(self.right_tree.tag_has("file_link"))) 656 | nb_folders = len(self.right_tree.tag_has("folder")) 657 | nb_folders += len(list(self.right_tree.tag_has("folder_link"))) 658 | files.sort(reverse=reverse, key=getsize) 659 | 660 | for index, item in enumerate(files): 661 | self.move_item(item, index + nb_folders) 662 | 663 | self.right_tree.heading("size", 664 | command=lambda: self._sort_by_size(not reverse)) 665 | 666 | def _sort_by_date(self, reverse): 667 | """Sort files and folders by modification date.""" 668 | files = list(self.right_tree.tag_has("file")) 669 | files.extend(list(self.right_tree.tag_has("file_link"))) 670 | folders = list(self.right_tree.tag_has("folder")) 671 | folders.extend(list(self.right_tree.tag_has("folder_link"))) 672 | l = len(folders) 673 | folders.sort(reverse=reverse, key=getmtime) 674 | files.sort(reverse=reverse, key=getmtime) 675 | 676 | for index, item in enumerate(folders): 677 | self.move_item(item, index) 678 | for index, item in enumerate(files): 679 | self.move_item(item, index + l) 680 | 681 | self.right_tree.heading("date", 682 | command=lambda: self._sort_by_date(not reverse)) 683 | 684 | # --- file selection 685 | def _file_selection_save(self, event): 686 | """Save mode only: put selected file name in name_entry.""" 687 | sel = self.right_tree.selection() 688 | if sel: 689 | sel = sel[0] 690 | tags = self.right_tree.item(sel, "tags") 691 | if ("file" in tags) or ("file_link" in tags): 692 | self.entry.delete(0, "end") 693 | if self.path_bar.winfo_ismapped(): 694 | self.entry.insert(0, self.right_tree.item(sel, "text")) 695 | else: 696 | # recently used files 697 | self.entry.insert(0, sel) 698 | self.entry.selection_clear() 699 | self.entry.icursor("end") 700 | 701 | def _file_selection_openfile(self, event): 702 | """Put selected file name in path_entry if visible.""" 703 | sel = self.right_tree.selection() 704 | if sel and self.entry.winfo_ismapped(): 705 | self.entry.delete(0, 'end') 706 | self.entry.insert("end", self.right_tree.item(sel[0], "text")) 707 | self.entry.selection_clear() 708 | self.entry.icursor("end") 709 | 710 | def _file_selection_opendir(self, event): 711 | """ 712 | Prevent selection of files in opendir mode and put selected folder 713 | name in path_entry if visible. 714 | """ 715 | sel = self.right_tree.selection() 716 | if sel: 717 | for s in sel: 718 | tags = self.right_tree.item(s, "tags") 719 | if ("file" in tags) or ("file_link" in tags): 720 | self.right_tree.selection_remove(s) 721 | sel = self.right_tree.selection() 722 | if len(sel) == 1 and self.entry.winfo_ismapped(): 723 | self.entry.delete(0, 'end') 724 | self.entry.insert("end", self.right_tree.item(sel[0], "text")) 725 | self.entry.selection_clear() 726 | self.entry.icursor("end") 727 | 728 | def _shortcut_select(self, event): 729 | """Selection of a shortcut (left pane).""" 730 | sel = self.left_tree.selection() 731 | if sel: 732 | sel = sel[0] 733 | if sel != "recent": 734 | self.display_folder(sel) 735 | else: 736 | self._display_recents() 737 | 738 | def _display_recents(self): 739 | """Display recently used files/folders.""" 740 | self.path_bar.grid_remove() 741 | self.right_tree.configure(displaycolumns=("location", "size", "date")) 742 | w = self.right_tree.winfo_width() - 305 743 | if w < 0: 744 | w = 250 745 | self.right_tree.column("#0", width=w) 746 | self.right_tree.column("location", stretch=False, width=100) 747 | self.right_tree.column("size", stretch=False, width=85) 748 | self.right_tree.column("date", width=120) 749 | if self.foldercreation: 750 | self.b_new_folder.grid_remove() 751 | extension = self.filetypes[self.filetype.get()] 752 | files = self._recent_files.get() 753 | self.right_tree.delete(*self.right_tree.get_children("")) 754 | i = 0 755 | if self.mode == "opendir": 756 | paths = [] 757 | for p in files: 758 | if isfile(p): 759 | p = dirname(p) 760 | d, f = split(p) 761 | tags = [str(i % 2)] 762 | vals = () 763 | if f: 764 | if f[0] == ".": 765 | tags.append("hidden") 766 | else: 767 | f = "/" 768 | if isdir(p): 769 | if islink(p): 770 | tags.append("folder_link") 771 | else: 772 | tags.append("folder") 773 | vals = (p, "", get_modification_date(p)) 774 | if vals and p not in paths: 775 | i += 1 776 | paths.append(p) 777 | self.right_tree.insert("", "end", p, text=f, tags=tags, 778 | values=vals) 779 | else: 780 | for p in files: 781 | d, f = split(p) 782 | tags = [str(i % 2)] 783 | vals = () 784 | if f: 785 | if f[0] == ".": 786 | tags.append("hidden") 787 | else: 788 | f = "/" 789 | if islink(p): 790 | if isfile(p): 791 | if extension == r".*$" or search(extension, f): 792 | tags.append("file_link") 793 | stats = stat(p) 794 | vals = (p, display_size(stats.st_size), 795 | display_modification_date(stats.st_mtime)) 796 | elif isdir(p): 797 | tags.append("folder_link") 798 | vals = (p, "", get_modification_date(p)) 799 | elif isfile(p): 800 | if extension == r".*$" or search(extension, f): 801 | tags.append("file") 802 | stats = stat(p) 803 | vals = (p, display_size(stats.st_size), 804 | display_modification_date(stats.st_mtime)) 805 | elif isdir(p): 806 | tags.append("folder") 807 | vals = (p, "", get_modification_date(p)) 808 | if vals: 809 | i += 1 810 | self.right_tree.insert("", "end", p, text=f, tags=tags, 811 | values=vals) 812 | 813 | def _select(self, event): 814 | """display folder content on double click / Enter, validate if file.""" 815 | sel = self.right_tree.selection() 816 | if sel: 817 | sel = sel[0] 818 | tags = self.right_tree.item(sel, "tags") 819 | if ("folder" in tags) or ("folder_link" in tags): 820 | self.display_folder(sel) 821 | elif self.mode != "opendir": 822 | self.validate(event) 823 | elif self.mode == "opendir": 824 | self.validate(event) 825 | 826 | def _unpost(self, event): 827 | """Hide self.key_browse_entry.""" 828 | if event.widget != self.key_browse_entry: 829 | self._key_browse_hide(event) 830 | 831 | def _hide_listbox(self, event): 832 | """Hide the path proposition listbox.""" 833 | if event.widget not in [self.listbox, self.entry, self.listbox_frame]: 834 | self.listbox_frame.place_forget() 835 | 836 | def _change_filetype(self): 837 | """Update view on filetype change.""" 838 | if self.path_bar.winfo_ismapped(): 839 | self.display_folder(self.history[self._hist_index]) 840 | else: 841 | self._display_recents() 842 | if self.mode == 'save': 843 | filename = self.entry.get() 844 | new_ext = self.filetypes[self.filetype.get()] 845 | if filename and not search(new_ext, filename): 846 | old_ext = search(r'\..+$', filename).group() 847 | exts = [e[2:].replace('\.', '.') for e in new_ext[:-1].split('|')] 848 | exts = [e for e in exts if search(r'\.[^\*]+$', e)] 849 | if exts: 850 | filename = filename.replace(old_ext, exts[0]) 851 | self.entry.delete(0, 'end') 852 | self.entry.insert(0, filename) 853 | 854 | # --- path completion in entries: key bindings 855 | def _down(self, event): 856 | """Focus listbox on Down arrow press in entry.""" 857 | self.listbox.focus_set() 858 | self.listbox.selection_set(0) 859 | 860 | def _tab(self, event): 861 | """Go to the end of selected text and remove selection on tab press.""" 862 | self.entry = event.widget 863 | self.entry.selection_clear() 864 | self.entry.icursor("end") 865 | return "break" 866 | 867 | def _select_enter(self, event, d): 868 | """Change entry content on Return key press in listbox.""" 869 | self.entry.delete(0, "end") 870 | self.entry.insert(0, join(d, self.listbox.selection_get())) 871 | self.entry.selection_clear() 872 | self.entry.focus_set() 873 | self.entry.icursor("end") 874 | 875 | def _select_mouse(self, event, d): 876 | """Change entry content on click in listbox.""" 877 | self.entry.delete(0, "end") 878 | self.entry.insert(0, join(d, self.listbox.get("@%i,%i" % (event.x, event.y)))) 879 | self.entry.selection_clear() 880 | self.entry.focus_set() 881 | self.entry.icursor("end") 882 | 883 | def _completion(self, action, modif, pos, prev_txt): 884 | """Complete the text in the path entry with existing folder/file names.""" 885 | if self.entry.selection_present(): 886 | sel = self.entry.selection_get() 887 | txt = prev_txt.replace(sel, '') 888 | else: 889 | txt = prev_txt 890 | if action == "0": 891 | self.listbox_frame.place_forget() 892 | txt = txt[:int(pos)] + txt[int(pos) + 1:] 893 | elif isabs(txt) or self.path_bar.winfo_ismapped(): 894 | txt = txt[:int(pos)] + modif + txt[int(pos):] 895 | d, f = split(txt) 896 | if f and not (f[0] == "." and self.hide): 897 | if not isabs(txt): 898 | d2 = join(self.history[self._hist_index], d) 899 | else: 900 | d2 = d 901 | 902 | try: 903 | root, dirs, files = walk(d2).send(None) 904 | dirs.sort(key=lambda n: n.lower()) 905 | l2 = [] 906 | if self.mode != "opendir": 907 | files.sort(key=lambda n: n.lower()) 908 | extension = self.filetypes[self.filetype.get()] 909 | if extension == r".*$": 910 | l2.extend([i.replace(" ", "\ ") for i in files if i[:len(f)] == f]) 911 | else: 912 | for i in files: 913 | if search(extension, i) and i[:len(f)] == f: 914 | l2.append(i.replace(" ", "\ ")) 915 | l2.extend([i.replace(" ", "\ ") + "/" for i in dirs if i[:len(f)] == f]) 916 | 917 | except StopIteration: 918 | # invalid content 919 | l2 = [] 920 | 921 | if len(l2) == 1: 922 | self.listbox_frame.place_forget() 923 | i = self.entry.index("insert") 924 | self.entry.delete(0, "end") 925 | self.entry.insert(0, join(d, l2[0])) 926 | self.entry.selection_range(i + 1, "end") 927 | self.entry.icursor(i + 1) 928 | 929 | elif len(l2) > 1: 930 | self.listbox.bind("", lambda e, arg=d: self._select_enter(e, arg)) 931 | self.listbox.bind("", lambda e, arg=d: self._select_mouse(e, arg)) 932 | self.listbox_var.set(" ".join(l2)) 933 | self.listbox_frame.lift() 934 | self.listbox.configure(height=len(l2)) 935 | self.listbox_frame.place(in_=self.entry, relx=0, rely=1, 936 | anchor="nw", relwidth=1) 937 | else: 938 | self.listbox_frame.place_forget() 939 | return True 940 | 941 | def _go_left(self, event): 942 | """Move focus to left pane.""" 943 | sel = self.left_tree.selection() 944 | if not sel: 945 | sel = expanduser("~") 946 | else: 947 | sel = sel[0] 948 | self.left_tree.focus_set() 949 | self.left_tree.focus(sel) 950 | 951 | # --- go to parent/children folder with Alt+Up/Down 952 | def _go_to_parent(self, event): 953 | """Go to parent directory.""" 954 | parent = dirname(self.path_var.get()) 955 | self.display_folder(parent, update_bar=False) 956 | 957 | def _go_to_child(self, event): 958 | """Go to child directory.""" 959 | lb = [b.get_value() for b in self.path_bar_buttons] 960 | i = lb.index(self.path_var.get()) 961 | if i < len(lb) - 1: 962 | self.display_folder(lb[i + 1], update_bar=False) 963 | 964 | # --- navigate in history with Alt+Left/ Right keys 965 | def _hist_backward(self, event): 966 | """Navigate backward in folder selection history.""" 967 | if self._hist_index > -len(self.history): 968 | self._hist_index -= 1 969 | self.display_folder(self.history[self._hist_index], reset=False) 970 | 971 | def _hist_forward(self, event): 972 | """Navigate forward in folder selection history.""" 973 | try: 974 | self.left_tree.selection_remove(*self.left_tree.selection()) 975 | except TypeError: 976 | # error raised in python 2 by empty selection 977 | pass 978 | if self._hist_index < -1: 979 | self._hist_index += 1 980 | self.display_folder(self.history[self._hist_index], reset=False) 981 | 982 | def _update_path_bar(self, path): 983 | """Update the buttons in path bar.""" 984 | for b in self.path_bar_buttons: 985 | b.destroy() 986 | self.path_bar_buttons = [] 987 | if path == "/": 988 | folders = [] 989 | else: 990 | folders = path.split(SEP) 991 | while '' in folders: 992 | folders.remove('') 993 | if OSNAME == 'nt': 994 | p = folders.pop(0) + '\\' 995 | b = PathButton(self.path_bar, self.path_var, p, text=p, 996 | command=lambda path=p: self.display_folder(path, update_bar=False)) 997 | else: 998 | p = "/" 999 | b = PathButton(self.path_bar, self.path_var, p, image=self.im_drive, 1000 | command=lambda path=p: self.display_folder(path, update_bar=False)) 1001 | self.path_bar_buttons.append(b) 1002 | b.grid(row=0, column=1, sticky="ns") 1003 | for i, folder in enumerate(folders): 1004 | p = join(p, folder) 1005 | b = PathButton(self.path_bar, self.path_var, p, text=folder, 1006 | command=lambda f=p: self.display_folder(f, update_bar=False), 1007 | style="path.tkfilebrowser.TButton") 1008 | self.path_bar_buttons.append(b) 1009 | b.grid(row=0, column=i + 2, sticky="ns") 1010 | 1011 | def _display_folder_listdir(self, folder, reset=True, update_bar=True): 1012 | """ 1013 | Display the content of folder in self.right_tree. 1014 | Arguments: 1015 | * reset (boolean): forget all the part of the history right of self._hist_index 1016 | * update_bar (boolean): update the buttons in path bar 1017 | """ 1018 | # remove trailing / if any 1019 | folder = abspath(folder) 1020 | # reorganize display if previous was 'recent' 1021 | if not self.path_bar.winfo_ismapped(): 1022 | self.path_bar.grid() 1023 | self.right_tree.configure(displaycolumns=("size", "date")) 1024 | w = self.right_tree.winfo_width() - 205 1025 | if w < 0: 1026 | w = 250 1027 | self.right_tree.column("#0", width=w) 1028 | self.right_tree.column("size", stretch=False, width=85) 1029 | self.right_tree.column("date", width=120) 1030 | if self.foldercreation: 1031 | self.b_new_folder.grid() 1032 | # reset history 1033 | if reset: 1034 | if not self._hist_index == -1: 1035 | self.history = self.history[:self._hist_index + 1] 1036 | self._hist_index = -1 1037 | self.history.append(folder) 1038 | # update path bar 1039 | if update_bar: 1040 | self._update_path_bar(folder) 1041 | self.path_var.set(folder) 1042 | # disable new folder creation if no write access 1043 | if self.foldercreation: 1044 | if access(folder, W_OK): 1045 | self.b_new_folder.state(('!disabled',)) 1046 | else: 1047 | self.b_new_folder.state(('disabled',)) 1048 | # clear self.right_tree 1049 | self.right_tree.delete(*self.right_tree.get_children("")) 1050 | self.right_tree.delete(*self.hidden) 1051 | self.hidden = () 1052 | root = folder 1053 | extension = self.filetypes[self.filetype.get()] 1054 | content = listdir(folder) 1055 | i = 0 1056 | for f in content: 1057 | p = join(root, f) 1058 | if f[0] == ".": 1059 | tags = ("hidden",) 1060 | if not self.hide: 1061 | tags = (str(i % 2),) 1062 | i += 1 1063 | else: 1064 | tags = (str(i % 2),) 1065 | i += 1 1066 | if isfile(p): 1067 | if extension == r".*$" or search(extension, f): 1068 | if islink(p): 1069 | tags = tags + ("file_link",) 1070 | else: 1071 | tags = tags + ("file",) 1072 | try: 1073 | stats = stat(p) 1074 | except OSError: 1075 | self.right_tree.insert("", "end", p, text=f, tags=tags, 1076 | values=("", "??", "??")) 1077 | else: 1078 | self.right_tree.insert("", "end", p, text=f, tags=tags, 1079 | values=("", 1080 | display_size(stats.st_size), 1081 | display_modification_date(stats.st_mtime))) 1082 | elif isdir(p): 1083 | if islink(p): 1084 | tags = tags + ("folder_link",) 1085 | else: 1086 | tags = tags + ("folder",) 1087 | 1088 | self.right_tree.insert("", "end", p, text=f, tags=tags, 1089 | values=("", "", get_modification_date(p))) 1090 | else: # broken link 1091 | tags = tags + ("link_broken",) 1092 | self.right_tree.insert("", "end", p, text=f, tags=tags, 1093 | values=("", "??", "??")) 1094 | 1095 | items = self.right_tree.get_children("") 1096 | if items: 1097 | self.right_tree.focus_set() 1098 | self.right_tree.focus(items[0]) 1099 | if self.hide: 1100 | self.hidden = self.right_tree.tag_has("hidden") 1101 | self.right_tree.detach(*self.right_tree.tag_has("hidden")) 1102 | self._sort_files_by_name(False) 1103 | 1104 | def _display_folder_walk(self, folder, reset=True, update_bar=True): 1105 | """ 1106 | Display the content of folder in self.right_tree. 1107 | Arguments: 1108 | * reset (boolean): forget all the part of the history right of self._hist_index 1109 | * update_bar (boolean): update the buttons in path bar 1110 | """ 1111 | # remove trailing / if any 1112 | folder = abspath(folder) 1113 | # reorganize display if previous was 'recent' 1114 | if not self.path_bar.winfo_ismapped(): 1115 | self.path_bar.grid() 1116 | self.right_tree.configure(displaycolumns=("size", "date")) 1117 | w = self.right_tree.winfo_width() - 205 1118 | if w < 0: 1119 | w = 250 1120 | self.right_tree.column("#0", width=w) 1121 | self.right_tree.column("size", stretch=False, width=85) 1122 | self.right_tree.column("date", width=120) 1123 | if self.foldercreation: 1124 | self.b_new_folder.grid() 1125 | # reset history 1126 | if reset: 1127 | if not self._hist_index == -1: 1128 | self.history = self.history[:self._hist_index + 1] 1129 | self._hist_index = -1 1130 | self.history.append(folder) 1131 | # update path bar 1132 | if update_bar: 1133 | self._update_path_bar(folder) 1134 | self.path_var.set(folder) 1135 | # disable new folder creation if no write access 1136 | if self.foldercreation: 1137 | if access(folder, W_OK): 1138 | self.b_new_folder.state(('!disabled',)) 1139 | else: 1140 | self.b_new_folder.state(('disabled',)) 1141 | # clear self.right_tree 1142 | self.right_tree.delete(*self.right_tree.get_children("")) 1143 | self.right_tree.delete(*self.hidden) 1144 | self.hidden = () 1145 | try: 1146 | root, dirs, files = walk(folder).send(None) 1147 | # display folders first 1148 | dirs.sort(key=lambda n: n.lower()) 1149 | i = 0 1150 | for d in dirs: 1151 | p = join(root, d) 1152 | if islink(p): 1153 | tags = ("folder_link",) 1154 | else: 1155 | tags = ("folder",) 1156 | if d[0] == ".": 1157 | tags = tags + ("hidden",) 1158 | if not self.hide: 1159 | tags = tags + (str(i % 2),) 1160 | i += 1 1161 | else: 1162 | tags = tags + (str(i % 2),) 1163 | i += 1 1164 | self.right_tree.insert("", "end", p, text=d, tags=tags, 1165 | values=("", "", get_modification_date(p))) 1166 | # display files 1167 | files.sort(key=lambda n: n.lower()) 1168 | extension = self.filetypes[self.filetype.get()] 1169 | for f in files: 1170 | if extension == r".*$" or search(extension, f): 1171 | p = join(root, f) 1172 | if islink(p): 1173 | tags = ("file_link",) 1174 | else: 1175 | tags = ("file",) 1176 | try: 1177 | stats = stat(p) 1178 | except FileNotFoundError: 1179 | stats = Stats(st_size="??", st_mtime="??") 1180 | tags = ("link_broken",) 1181 | if f[0] == ".": 1182 | tags = tags + ("hidden",) 1183 | if not self.hide: 1184 | tags = tags + (str(i % 2),) 1185 | i += 1 1186 | else: 1187 | tags = tags + (str(i % 2),) 1188 | i += 1 1189 | 1190 | self.right_tree.insert("", "end", p, text=f, tags=tags, 1191 | values=("", 1192 | display_size(stats.st_size), 1193 | display_modification_date(stats.st_mtime))) 1194 | items = self.right_tree.get_children("") 1195 | if items: 1196 | self.right_tree.focus_set() 1197 | self.right_tree.focus(items[0]) 1198 | if self.hide: 1199 | self.hidden = self.right_tree.tag_has("hidden") 1200 | self.right_tree.detach(*self.right_tree.tag_has("hidden")) 1201 | except StopIteration: 1202 | self._display_folder_listdir(folder, reset, update_bar) 1203 | except PermissionError as e: 1204 | cst.showerror('PermissionError', str(e), master=self) 1205 | 1206 | def _display_folder_scandir(self, folder, reset=True, update_bar=True): 1207 | """ 1208 | Display the content of folder in self.right_tree. 1209 | 1210 | Arguments: 1211 | * reset (boolean): forget all the part of the history right of self._hist_index 1212 | * update_bar (boolean): update the buttons in path bar 1213 | """ 1214 | # remove trailing / if any 1215 | folder = abspath(folder) 1216 | # reorganize display if previous was 'recent' 1217 | if not self.path_bar.winfo_ismapped(): 1218 | self.path_bar.grid() 1219 | self.right_tree.configure(displaycolumns=("size", "date")) 1220 | w = self.right_tree.winfo_width() - 205 1221 | if w < 0: 1222 | w = 250 1223 | self.right_tree.column("#0", width=w) 1224 | self.right_tree.column("size", stretch=False, width=85) 1225 | self.right_tree.column("date", width=120) 1226 | if self.foldercreation: 1227 | self.b_new_folder.grid() 1228 | # reset history 1229 | if reset: 1230 | if not self._hist_index == -1: 1231 | self.history = self.history[:self._hist_index + 1] 1232 | self._hist_index = -1 1233 | self.history.append(folder) 1234 | # update path bar 1235 | if update_bar: 1236 | self._update_path_bar(folder) 1237 | self.path_var.set(folder) 1238 | # disable new folder creation if no write access 1239 | if self.foldercreation: 1240 | if access(folder, W_OK): 1241 | self.b_new_folder.state(('!disabled',)) 1242 | else: 1243 | self.b_new_folder.state(('disabled',)) 1244 | # clear self.right_tree 1245 | self.right_tree.delete(*self.right_tree.get_children("")) 1246 | self.right_tree.delete(*self.hidden) 1247 | self.hidden = () 1248 | extension = self.filetypes[self.filetype.get()] 1249 | try: 1250 | content = sorted(scandir(folder), key=key_sort_files) 1251 | i = 0 1252 | tags_array = [["folder", "folder_link"], 1253 | ["file", "file_link"]] 1254 | for f in content: 1255 | b_file = f.is_file() 1256 | name = f.name 1257 | try: 1258 | stats = f.stat() 1259 | tags = (tags_array[b_file][f.is_symlink()],) 1260 | except FileNotFoundError: 1261 | stats = Stats(st_size="??", st_mtime="??") 1262 | tags = ("link_broken",) 1263 | if name[0] == '.': 1264 | tags = tags + ("hidden",) 1265 | if not self.hide: 1266 | tags = tags + (str(i % 2),) 1267 | i += 1 1268 | else: 1269 | tags = tags + (str(i % 2),) 1270 | i += 1 1271 | if b_file: 1272 | if extension == r".*$" or search(extension, name): 1273 | self.right_tree.insert("", "end", f.path, text=name, tags=tags, 1274 | values=("", 1275 | display_size(stats.st_size), 1276 | display_modification_date(stats.st_mtime))) 1277 | else: 1278 | self.right_tree.insert("", "end", f.path, text=name, tags=tags, 1279 | values=("", "", 1280 | display_modification_date(stats.st_mtime))) 1281 | items = self.right_tree.get_children("") 1282 | if items: 1283 | self.right_tree.focus_set() 1284 | self.right_tree.focus(items[0]) 1285 | if self.hide: 1286 | self.hidden = self.right_tree.tag_has("hidden") 1287 | self.right_tree.detach(*self.right_tree.tag_has("hidden")) 1288 | except FileNotFoundError: 1289 | self._display_folder_scandir(expanduser('~'), reset=True, update_bar=True) 1290 | except PermissionError as e: 1291 | cst.showerror('PermissionError', str(e), master=self) 1292 | 1293 | def create_folder(self, event=None): 1294 | """Create new folder in current location.""" 1295 | def ok(event): 1296 | name = e.get() 1297 | e.destroy() 1298 | if name: 1299 | folder = join(path, name) 1300 | try: 1301 | mkdir(folder) 1302 | except Exception: 1303 | # show exception to the user (typically PermissionError or FileExistsError) 1304 | cst.showerror(_("Error"), traceback.format_exc()) 1305 | self.display_folder(path) 1306 | 1307 | def cancel(event): 1308 | e.destroy() 1309 | self.right_tree.delete("tmp") 1310 | 1311 | path = self.path_var.get() 1312 | 1313 | if self.path_bar.winfo_ismapped() and access(path, W_OK): 1314 | self.right_tree.insert("", 0, "tmp", tags=("folder", "1")) 1315 | self.right_tree.see("tmp") 1316 | e = ttk.Entry(self) 1317 | x, y, w, h = self.right_tree.bbox("tmp", column="#0") 1318 | e.place(in_=self.right_tree, x=x + 24, y=y, 1319 | width=w - x - 4) 1320 | e.bind("", ok) 1321 | e.bind("", cancel) 1322 | e.bind("", cancel) 1323 | e.focus_set() 1324 | 1325 | def move_item(self, item, index): 1326 | """Move item to index and update dark/light line alternance.""" 1327 | self.right_tree.move(item, "", index) 1328 | tags = [t for t in self.right_tree.item(item, 'tags') 1329 | if t not in ['1', '0']] 1330 | tags.append(str(index % 2)) 1331 | self.right_tree.item(item, tags=tags) 1332 | 1333 | def toggle_path_entry(self, event): 1334 | """Toggle visibility of path entry.""" 1335 | if self.entry.winfo_ismapped(): 1336 | self.entry.grid_remove() 1337 | self.entry.delete(0, "end") 1338 | else: 1339 | self.entry.grid() 1340 | self.entry.focus_set() 1341 | 1342 | def toggle_hidden(self, event=None): 1343 | """Toggle the visibility of hidden files/folders.""" 1344 | if self.hide: 1345 | self.hide = False 1346 | for item in reversed(self.hidden): 1347 | self.right_tree.move(item, "", 0) 1348 | self.hidden = () 1349 | else: 1350 | self.hide = True 1351 | self.hidden = self.right_tree.tag_has("hidden") 1352 | self.right_tree.detach(*self.right_tree.tag_has("hidden")) 1353 | # restore color alternance 1354 | for i, item in enumerate(self.right_tree.get_children("")): 1355 | tags = [t for t in self.right_tree.item(item, 'tags') 1356 | if t not in ['1', '0']] 1357 | tags.append(str(i % 2)) 1358 | self.right_tree.item(item, tags=tags) 1359 | 1360 | def get_result(self): 1361 | """Return selection.""" 1362 | return self.result 1363 | 1364 | def quit(self): 1365 | """Destroy dialog.""" 1366 | self.destroy() 1367 | if self.result: 1368 | if isinstance(self.result, tuple): 1369 | for path in self.result: 1370 | self._recent_files.add(path) 1371 | else: 1372 | self._recent_files.add(self.result) 1373 | 1374 | def _validate_save(self): 1375 | """Validate selection in save mode.""" 1376 | name = self.entry.get() 1377 | if name: 1378 | ext = splitext(name)[-1] 1379 | if not ext and not name[-1] == "/": 1380 | # append default extension if none given 1381 | name += self.defaultext 1382 | if isabs(name): 1383 | # name is an absolute path 1384 | if exists(dirname(name)): 1385 | rep = True 1386 | if isfile(name): 1387 | rep = cst.askyesnocancel(_("Confirmation"), 1388 | _("The file {file} already exists, do you want to replace it?").format(file=name), 1389 | icon="warning") 1390 | elif isdir(name): 1391 | # it's a directory 1392 | rep = False 1393 | self.display_folder(name) 1394 | path = name 1395 | else: 1396 | # the path is invalid 1397 | rep = False 1398 | elif self.path_bar.winfo_ismapped(): 1399 | # we are not in the "recent files" 1400 | path = join(self.history[self._hist_index], name) 1401 | rep = True 1402 | if exists(path): 1403 | if isfile(path): 1404 | rep = cst.askyesnocancel(_("Confirmation"), 1405 | _("The file {file} already exists, do you want to replace it?").format(file=name), 1406 | icon="warning") 1407 | else: 1408 | # it's a directory 1409 | rep = False 1410 | self.display_folder(path) 1411 | elif not exists(dirname(path)): 1412 | # the path is invalid 1413 | rep = False 1414 | else: 1415 | # recently used file 1416 | sel = self.right_tree.selection() 1417 | if len(sel) == 1: 1418 | path = sel[0] 1419 | tags = self.right_tree.item(sel, "tags") 1420 | if ("folder" in tags) or ("folder_link" in tags): 1421 | rep = False 1422 | self.display_folder(path) 1423 | elif isfile(path): 1424 | rep = cst.askyesnocancel(_("Confirmation"), 1425 | _("The file {file} already exists, do you want to replace it?").format(file=name), 1426 | icon="warning") 1427 | else: 1428 | rep = True 1429 | else: 1430 | rep = False 1431 | 1432 | if rep: 1433 | self.result = realpath(path) 1434 | self.quit() 1435 | elif rep is None: 1436 | self.quit() 1437 | else: 1438 | self.entry.delete(0, "end") 1439 | self.entry.focus_set() 1440 | 1441 | def _validate_from_entry(self): 1442 | """ 1443 | Validate selection from path entry in open mode. 1444 | 1445 | Return False if the entry is empty, True otherwise. 1446 | """ 1447 | name = self.entry.get() 1448 | if name: # get file/folder from entry 1449 | if not isabs(name) and self.path_bar.winfo_ismapped(): 1450 | # we are not in the "recent files" 1451 | name = join(self.history[self._hist_index], name) 1452 | if not exists(name): 1453 | self.entry.delete(0, "end") 1454 | elif self.mode == "openfile": 1455 | if isfile(name): 1456 | if self.multiple_selection: 1457 | self.result = (realpath(name),) 1458 | else: 1459 | self.result = realpath(name) 1460 | self.quit() 1461 | else: 1462 | self.display_folder(name) 1463 | self.entry.grid_remove() 1464 | self.entry.delete(0, "end") 1465 | else: 1466 | if self.multiple_selection: 1467 | self.result = (realpath(name),) 1468 | else: 1469 | self.result = realpath(name) 1470 | self.quit() 1471 | return True 1472 | else: 1473 | return False 1474 | 1475 | def _validate_multiple_sel(self): 1476 | """Validate selection in open mode with multiple selection.""" 1477 | sel = self.right_tree.selection() 1478 | if self.mode == "openfile": 1479 | if len(sel) == 1: 1480 | sel = sel[0] 1481 | tags = self.right_tree.item(sel, "tags") 1482 | if ("folder" in tags) or ("folder_link" in tags): 1483 | self.display_folder(sel) 1484 | else: 1485 | self.result = (realpath(sel),) 1486 | self.quit() 1487 | elif len(sel) > 1: 1488 | files = tuple(s for s in sel if "file" in self.right_tree.item(s, "tags")) 1489 | files = files + tuple(realpath(s) for s in sel if "file_link" in self.right_tree.item(s, "tags")) 1490 | if files: 1491 | self.result = files 1492 | self.quit() 1493 | else: 1494 | self.right_tree.selection_remove(*sel) 1495 | else: 1496 | if sel: 1497 | self.result = tuple(realpath(s) for s in sel) 1498 | else: 1499 | self.result = (realpath(self.history[self._hist_index]),) 1500 | self.quit() 1501 | 1502 | def _validate_single_sel(self): 1503 | """Validate selection in open mode without multiple selection.""" 1504 | sel = self.right_tree.selection() 1505 | if self.mode == "openfile": 1506 | if len(sel) == 1: 1507 | sel = sel[0] 1508 | tags = self.right_tree.item(sel, "tags") 1509 | if ("folder" in tags) or ("folder_link" in tags): 1510 | self.display_folder(sel) 1511 | else: 1512 | self.result = realpath(sel) 1513 | self.quit() 1514 | elif self.mode == "opendir": 1515 | if len(sel) == 1: 1516 | self.result = realpath(sel[0]) 1517 | else: 1518 | self.result = realpath(self.history[self._hist_index]) 1519 | self.quit() 1520 | else: # mode is "openpath" 1521 | if len(sel) == 1: 1522 | self.result = realpath(sel[0]) 1523 | self.quit() 1524 | 1525 | def validate(self, event=None): 1526 | """Validate selection and store it in self.results if valid.""" 1527 | if self.mode == "save": 1528 | self._validate_save() 1529 | else: 1530 | validation = self._validate_from_entry() 1531 | if not validation: 1532 | # the entry is empty 1533 | if self.multiple_selection: 1534 | self._validate_multiple_sel() 1535 | else: 1536 | self._validate_single_sel() 1537 | -------------------------------------------------------------------------------- /tkfilebrowser/functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkfilebrowser - Alternative to filedialog for Tkinter 4 | Copyright 2017-2018 Juliette Monsel 5 | 6 | tkfilebrowser is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkfilebrowser is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | 20 | Functions 21 | """ 22 | 23 | 24 | from tkfilebrowser.constants import _ 25 | from tkfilebrowser.filebrowser import FileBrowser 26 | 27 | 28 | def askopenpathname(parent=None, title=_("Open"), **kwargs): 29 | """ 30 | Return :obj:`''` or the absolute path of the chosen path (file or directory). 31 | 32 | Arguments: 33 | 34 | parent : Tk or Toplevel instance 35 | parent window 36 | 37 | title : str 38 | the title of the filebrowser window 39 | 40 | initialdir : str 41 | directory whose content is initially displayed 42 | 43 | initialfile : str 44 | initially selected item (just the name, not the full path) 45 | 46 | filetypes : list :obj:`[("name", "*.ext1|*.ext2|.."), ...]` 47 | only the files of given filetype will be displayed, 48 | e.g. to allow the user to switch between displaying only PNG or JPG 49 | pictures or dispalying all files: 50 | :obj:`filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")]` 51 | 52 | okbuttontext : str 53 | text displayed on the validate button, default is "Open". 54 | 55 | cancelbuttontext : str 56 | text displayed on the button that cancels the selection, default is "Cancel". 57 | 58 | foldercreation : bool 59 | enable the user to create new folders if True (default) 60 | """ 61 | dialog = FileBrowser(parent, mode="openpath", multiple_selection=False, 62 | title=title, **kwargs) 63 | dialog.wait_window(dialog) 64 | return dialog.get_result() 65 | 66 | 67 | def askopenpathnames(parent=None, title=_("Open"), **kwargs): 68 | """ 69 | Return :obj:`()` or the tuple of the absolute paths of the chosen paths (files and directories) 70 | 71 | Arguments: 72 | 73 | parent : Tk or Toplevel instance 74 | parent window 75 | 76 | title : str 77 | the title of the filebrowser window 78 | 79 | initialdir : str 80 | directory whose content is initially displayed 81 | 82 | initialfile : str 83 | initially selected item (just the name, not the full path) 84 | 85 | filetypes : list :obj:`[("name", "*.ext1|*.ext2|.."), ...]` 86 | only the files of given filetype will be displayed, 87 | e.g. to allow the user to switch between displaying only PNG or JPG 88 | pictures or dispalying all files: 89 | :obj:`filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")]` 90 | 91 | okbuttontext : str 92 | text displayed on the validate button, default is "Open". 93 | 94 | cancelbuttontext : str 95 | text displayed on the button that cancels the selection, default is "Cancel". 96 | 97 | foldercreation : bool 98 | enable the user to create new folders if True (default) 99 | """ 100 | dialog = FileBrowser(parent, mode="openpath", multiple_selection=True, 101 | title=title, **kwargs) 102 | dialog.wait_window(dialog) 103 | res = dialog.get_result() 104 | if not res: # type consistency: always return a tuple 105 | res = () 106 | return res 107 | 108 | 109 | def askopendirname(parent=None, title=_("Open"), **kwargs): 110 | """ 111 | Return :obj:`''` or the absolute path of the chosen directory. 112 | 113 | Arguments: 114 | 115 | parent : Tk or Toplevel instance 116 | parent window 117 | 118 | title : str 119 | the title of the filebrowser window 120 | 121 | initialdir : str 122 | directory whose content is initially displayed 123 | 124 | initialfile : str 125 | initially selected item (just the name, not the full path) 126 | 127 | filetypes : list :obj:`[("name", "*.ext1|*.ext2|.."), ...]` 128 | only the files of given filetype will be displayed, 129 | e.g. to allow the user to switch between displaying only PNG or JPG 130 | pictures or dispalying all files: 131 | :obj:`filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")]` 132 | 133 | okbuttontext : str 134 | text displayed on the validate button, default is "Open". 135 | 136 | cancelbuttontext : str 137 | text displayed on the button that cancels the selection, default is "Cancel". 138 | 139 | foldercreation : bool 140 | enable the user to create new folders if True (default) 141 | """ 142 | dialog = FileBrowser(parent, mode="opendir", multiple_selection=False, 143 | title=title, **kwargs) 144 | dialog.wait_window(dialog) 145 | return dialog.get_result() 146 | 147 | 148 | def askopendirnames(parent=None, title=_("Open"), **kwargs): 149 | """ 150 | Return :obj:`()` or the tuple of the absolute paths of the chosen directories 151 | 152 | Arguments: 153 | 154 | parent : Tk or Toplevel instance 155 | parent window 156 | 157 | title : str 158 | the title of the filebrowser window 159 | 160 | initialdir : str 161 | directory whose content is initially displayed 162 | 163 | initialfile : str 164 | initially selected item (just the name, not the full path) 165 | 166 | filetypes : list :obj:`[("name", "*.ext1|*.ext2|.."), ...]` 167 | only the files of given filetype will be displayed, 168 | e.g. to allow the user to switch between displaying only PNG or JPG 169 | pictures or dispalying all files: 170 | :obj:`filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")]` 171 | 172 | okbuttontext : str 173 | text displayed on the validate button, default is "Open". 174 | 175 | cancelbuttontext : str 176 | text displayed on the button that cancels the selection, default is "Cancel". 177 | 178 | foldercreation : bool 179 | enable the user to create new folders if True (default) 180 | """ 181 | dialog = FileBrowser(parent, mode="opendir", multiple_selection=True, 182 | title=title, **kwargs) 183 | dialog.wait_window(dialog) 184 | res = dialog.get_result() 185 | if not res: # type consistency: always return a tuple 186 | res = () 187 | return res 188 | 189 | 190 | def askopenfilename(parent=None, title=_("Open"), **kwargs): 191 | """ 192 | Return :obj:`''` or the absolute path of the chosen file 193 | 194 | Arguments: 195 | 196 | parent : Tk or Toplevel instance 197 | parent window 198 | 199 | title : str 200 | the title of the filebrowser window 201 | 202 | initialdir : str 203 | directory whose content is initially displayed 204 | 205 | initialfile : str 206 | initially selected item (just the name, not the full path) 207 | 208 | filetypes : list :obj:`[("name", "*.ext1|*.ext2|.."), ...]` 209 | only the files of given filetype will be displayed, 210 | e.g. to allow the user to switch between displaying only PNG or JPG 211 | pictures or dispalying all files: 212 | :obj:`filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")]` 213 | 214 | okbuttontext : str 215 | text displayed on the validate button, default is "Open". 216 | 217 | cancelbuttontext : str 218 | text displayed on the button that cancels the selection, default is "Cancel". 219 | 220 | foldercreation : bool 221 | enable the user to create new folders if True (default) 222 | """ 223 | dialog = FileBrowser(parent, mode="openfile", multiple_selection=False, 224 | title=title, **kwargs) 225 | dialog.wait_window(dialog) 226 | return dialog.get_result() 227 | 228 | 229 | def askopenfilenames(parent=None, title=_("Open"), **kwargs): 230 | """ 231 | Return :obj:`()` or the tuple of the absolute paths of the chosen files 232 | 233 | Arguments: 234 | 235 | parent : Tk or Toplevel instance 236 | parent window 237 | 238 | title : str 239 | the title of the filebrowser window 240 | 241 | initialdir : str 242 | directory whose content is initially displayed 243 | 244 | initialfile : str 245 | initially selected item (just the name, not the full path) 246 | 247 | filetypes : list :obj:`[("name", "*.ext1|*.ext2|.."), ...]` 248 | only the files of given filetype will be displayed, 249 | e.g. to allow the user to switch between displaying only PNG or JPG 250 | pictures or dispalying all files: 251 | :obj:`filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")]` 252 | 253 | okbuttontext : str 254 | text displayed on the validate button, default is "Open". 255 | 256 | cancelbuttontext : str 257 | text displayed on the button that cancels the selection, default is "Cancel". 258 | 259 | foldercreation : bool 260 | enable the user to create new folders if True (default) 261 | """ 262 | dialog = FileBrowser(parent, mode="openfile", multiple_selection=True, 263 | title=title, **kwargs) 264 | dialog.wait_window(dialog) 265 | res = dialog.get_result() 266 | if not res: # type consistency: always return a tuple 267 | res = () 268 | return res 269 | 270 | 271 | def asksaveasfilename(parent=None, title=_("Save As"), **kwargs): 272 | """ 273 | Return :obj:`''` or the chosen absolute path (the file might not exist) 274 | 275 | Arguments: 276 | 277 | parent : Tk or Toplevel instance 278 | parent window 279 | 280 | title : str 281 | the title of the filebrowser window 282 | 283 | initialdir : str 284 | directory whose content is initially displayed 285 | 286 | initialfile : str 287 | initially selected item (just the name, not the full path) 288 | 289 | defaultext : str (e.g. '.png') 290 | extension added to filename if none is given (default is none) 291 | 292 | filetypes : list :obj:`[("name", "*.ext1|*.ext2|.."), ...]` 293 | only the files of given filetype will be displayed, 294 | e.g. to allow the user to switch between displaying only PNG or JPG 295 | pictures or dispalying all files: 296 | :obj:`filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")]` 297 | 298 | okbuttontext : str 299 | text displayed on the validate button, default is "Open". 300 | 301 | cancelbuttontext : str 302 | text displayed on the button that cancels the selection, default is "Cancel". 303 | 304 | foldercreation : bool 305 | enable the user to create new folders if True (default) 306 | """ 307 | dialog = FileBrowser(parent, mode="save", title=title, **kwargs) 308 | dialog.wait_window(dialog) 309 | return dialog.get_result() 310 | -------------------------------------------------------------------------------- /tkfilebrowser/images/desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/desktop.png -------------------------------------------------------------------------------- /tkfilebrowser/images/drive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/drive.png -------------------------------------------------------------------------------- /tkfilebrowser/images/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/file.png -------------------------------------------------------------------------------- /tkfilebrowser/images/file_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/file_link.png -------------------------------------------------------------------------------- /tkfilebrowser/images/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/folder.png -------------------------------------------------------------------------------- /tkfilebrowser/images/folder_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/folder_link.png -------------------------------------------------------------------------------- /tkfilebrowser/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/home.png -------------------------------------------------------------------------------- /tkfilebrowser/images/link_broken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/link_broken.png -------------------------------------------------------------------------------- /tkfilebrowser/images/new_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/new_folder.png -------------------------------------------------------------------------------- /tkfilebrowser/images/recent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/recent.png -------------------------------------------------------------------------------- /tkfilebrowser/images/recent_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4321/tkFileBrowser/5f21cdcdc1805abdc7d6300b057be41fc56bc3e2/tkfilebrowser/images/recent_24.png -------------------------------------------------------------------------------- /tkfilebrowser/path_button.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkfilebrowser - Alternative to filedialog for Tkinter 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkfilebrowser is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkfilebrowser is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | 20 | Path bar button class 21 | """ 22 | 23 | 24 | from tkfilebrowser.constants import add_trace, remove_trace, ttk 25 | 26 | 27 | class PathButton(ttk.Button): 28 | """Toggle button class to make the path bar.""" 29 | 30 | def __init__(self, parent, variable, value, **kwargs): 31 | """ 32 | Create a PathButton. 33 | 34 | Like Radiobuttons, only one PathButton in the group (all PathButtons 35 | sharing the same control variable) can be selected. 36 | 37 | Options: 38 | * parent: parent widget 39 | * variable: control variable that the PathButton shares with the 40 | other PathButtons in the group (like for Radiobuttons) 41 | * value: when the PathButton is clicked, the control variable is set 42 | to value 43 | * all ttk.Button options 44 | """ 45 | kwargs["style"] = "path.tkfilebrowser.TButton" 46 | kwargs.setdefault("text", "") 47 | txt = kwargs['text'] 48 | kwargs.setdefault("width", len(txt) + 1 + txt.count('m') + txt.count('M')) 49 | ttk.Button.__init__(self, parent, **kwargs) 50 | self.variable = variable 51 | self.value = value 52 | self._trace = add_trace(self.variable, "write", self.var_change) 53 | self.bind("", self.on_press) 54 | 55 | def on_press(self, event): 56 | """Change the control variable value when the button is pressed.""" 57 | self.variable.set(self.value) 58 | 59 | def get_value(self): 60 | """Return value.""" 61 | return self.value 62 | 63 | def destroy(self): 64 | """Remove trace from variable and destroy widget.""" 65 | remove_trace(self.variable, "write", self._trace) 66 | ttk.Button.destroy(self) 67 | 68 | def var_change(self, *args): 69 | """Change the state of the button when the control variable changes.""" 70 | self.master.update() 71 | self.master.update_idletasks() 72 | if self.variable.get() == self.value: 73 | self.state(("selected",)) 74 | else: 75 | self.state(("!selected",)) 76 | -------------------------------------------------------------------------------- /tkfilebrowser/recent_files.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkfilebrowser - Alternative to filedialog for Tkinter 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkfilebrowser is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkfilebrowser is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | 20 | The icons are modified versions of icons from the elementary project 21 | (the xfce fork to be precise https://github.com/shimmerproject/elementary-xfce) 22 | Copyright 2007-2013 elementary LLC. 23 | 24 | 25 | Recent files management 26 | """ 27 | 28 | 29 | class RecentFiles: 30 | """Recent files manager.""" 31 | def __init__(self, filename, nbmax=30): 32 | """ 33 | Create a recent file manager. 34 | 35 | Options: 36 | * filename: file where the recent file list is read/saved 37 | * nbmax: maximum number of recent files to remember 38 | """ 39 | self._filename = filename 40 | self.nbmax = nbmax 41 | self._files = [] # most recent files first 42 | try: 43 | with open(filename) as file: 44 | self._files = file.read().splitlines() 45 | except Exception: 46 | pass 47 | 48 | def get(self): 49 | """Return recent file list.""" 50 | return self._files 51 | 52 | def add(self, file): 53 | """Add file to recent files.""" 54 | if file not in self._files: 55 | self._files.insert(0, file) 56 | if len(self._files) > self.nbmax: 57 | del(self._files[-1]) 58 | else: 59 | self._files.remove(file) 60 | self._files.insert(0, file) 61 | try: 62 | with open(self._filename, 'w') as file: 63 | file.write('\n'.join(self._files)) 64 | except Exception: 65 | # avoid raising errors if location is read-only or invalid path 66 | pass 67 | -------------------------------------------------------------------------------- /tkfilebrowser/tooltip.py: -------------------------------------------------------------------------------- 1 | # *** coding: utf-8 -*- 2 | """ 3 | tkfilebrowser - Alternative to filedialog for Tkinter 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkfilebrowser is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkfilebrowser is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | 20 | Tooltip and TooltipTreeWrapper classes to display the full path of a shortcut 21 | when the mouse stays over long enough 22 | """ 23 | 24 | 25 | from tkfilebrowser.constants import tk, ttk 26 | from sys import platform 27 | 28 | 29 | class Tooltip(tk.Toplevel): 30 | """Tooltip to display when the mouse stays long enough on an item.""" 31 | def __init__(self, parent, **kwargs): 32 | """ 33 | Create Tooltip. 34 | 35 | Options: 36 | * parent: parent window 37 | * text: text (str) to display in the tooltip 38 | * compound: relative orientation of the graphic relative to the text 39 | * alpha: opacity of the tooltip (0 for transparent, 1 for opaque), 40 | the text is affected too, so 0 would mean an invisible tooltip 41 | """ 42 | tk.Toplevel.__init__(self, parent) 43 | self.transient(parent) 44 | if platform.startswith('linux'): 45 | self.attributes('-type', 'tooltip') 46 | self.attributes('-alpha', kwargs.get('alpha', 0.8)) 47 | self.overrideredirect(True) 48 | style = kwargs.get('style', 'tooltip.tkfilebrowser.TLabel') 49 | 50 | bg = ttk.Style(self).lookup(style, 'background') 51 | self.configure(background=bg) 52 | 53 | self.label = ttk.Label(self, text=kwargs.get('text', ''), 54 | style=style, compound=kwargs.get('compound', 'left'), 55 | padding=kwargs.get('padding', 4)) 56 | self.label.pack() 57 | 58 | def configure(self, **kwargs): 59 | if 'text' in kwargs: 60 | self.label.configure(text=kwargs.pop('text')) 61 | if 'image' in kwargs: 62 | self.label.configure(image=kwargs.pop('image')) 63 | if 'alpha' in kwargs: 64 | self.attributes('-alpha', kwargs.pop('alpha')) 65 | tk.Toplevel.configure(self, **kwargs) 66 | 67 | 68 | class TooltipTreeWrapper: 69 | """Tooltip wrapper for a Treeview.""" 70 | def __init__(self, tree, delay=1500, **kwargs): 71 | """ 72 | Create a Tooltip wrapper for the Treeview tree. 73 | 74 | This wrapper enables the creation of tooltips for tree's items with all 75 | the bindings to make them appear/disappear. 76 | 77 | Options: 78 | * tree: wrapped Treeview 79 | * delay: hover delay before displaying the tooltip (ms) 80 | * all keyword arguments of a Tooltip 81 | """ 82 | self.tree = tree 83 | self.delay = delay 84 | self._timer_id = '' 85 | self.tooltip_text = {} 86 | self.tooltip = Tooltip(tree, **kwargs) 87 | self.tooltip.withdraw() 88 | self.current_item = None 89 | 90 | self.tree.bind('', self._on_motion) 91 | self.tree.bind('', self._on_leave) 92 | 93 | def _on_leave(self, event): 94 | try: 95 | self.tree.after_cancel(self._timer_id) 96 | except ValueError: 97 | # nothing to cancel 98 | pass 99 | 100 | def add_tooltip(self, item, text): 101 | """Add a tooltip with given text to the item.""" 102 | self.tooltip_text[item] = text 103 | 104 | def _on_motion(self, event): 105 | """Withdraw tooltip on mouse motion and cancel its appearance.""" 106 | if self.tooltip.winfo_ismapped(): 107 | x, y = self.tree.winfo_pointerxy() 108 | if self.tree.winfo_containing(x, y) != self.tooltip: 109 | if self.tree.identify_row(y - self.tree.winfo_rooty()): 110 | self.tooltip.withdraw() 111 | self.current_item = None 112 | else: 113 | try: 114 | self.tree.after_cancel(self._timer_id) 115 | except ValueError: 116 | # nothing to cancel 117 | pass 118 | self._timer_id = self.tree.after(self.delay, self.display_tooltip) 119 | 120 | def display_tooltip(self): 121 | """Display the tooltip corresponding to the hovered item.""" 122 | item = self.tree.identify_row(self.tree.winfo_pointery() - self.tree.winfo_rooty()) 123 | text = self.tooltip_text.get(item, '') 124 | self.current_item = item 125 | if text: 126 | self.tooltip.configure(text=text) 127 | self.tooltip.deiconify() 128 | x = self.tree.winfo_pointerx() + 14 129 | y = self.tree.winfo_rooty() + self.tree.bbox(item)[1] + self.tree.bbox(item)[3] 130 | self.tooltip.geometry('+%i+%i' % (x, y)) 131 | --------------------------------------------------------------------------------