├── .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 |
--------------------------------------------------------------------------------