pax_global_header00006660000000000000000000000064141537135420014517gustar00rootroot0000000000000052 comment=7d0ec0aae44961a452f4ae4d57e3cdb8bf33b2d9 pympress-1.7.1/000077500000000000000000000000001415371354200134075ustar00rootroot00000000000000pympress-1.7.1/.github/000077500000000000000000000000001415371354200147475ustar00rootroot00000000000000pympress-1.7.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001415371354200171325ustar00rootroot00000000000000pympress-1.7.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000035331415371354200216300ustar00rootroot00000000000000--- name: Bug report about: Report a problem in pympress title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** - OS: [e.g. Ubuntu] - Python version: [e.g. 3.9] - Pympress version: [e.g. 1.5.0] - Installation method: [e.g. source, pip, binary installer, chocolatey, copr, other package manager] **Debug information (see below for file locations)** - What is reported in pympress.log? - Does the problem still happen if you remove your config file? (You can just move the config file to a different location to be able to restore it after testing) **Additional context** Add any other context about the problem here. pympress-1.7.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003121415371354200211160ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: Discussions url: https://github.com/Cimbali/pympress/discussions about: To ask and answer questions that don’t fit in the categories above. pympress-1.7.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011301415371354200226520ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for pympress title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. pympress-1.7.1/.github/workflows/000077500000000000000000000000001415371354200170045ustar00rootroot00000000000000pympress-1.7.1/.github/workflows/deploy_doc_l10n.yml000066400000000000000000000045111415371354200225030ustar00rootroot00000000000000name: Update docs and translatable strings on: push: branches: - master jobs: strings: name: Upload translatable strings runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.x' - name: Install dependencies run: | sudo apt-get update -q sudo apt-get install -qy jq pip install -e .[babel] - name: Extract run: python setup.py extract_messages - name: Upload env: poeditor_api_token: ${{ secrets.POEDITOR_API_TOKEN }} run: ./scripts/poedit.sh upload build: name: Generate docs needs: strings runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.x' - name: Install dependencies run: | sudo apt-get update -q sudo apt-get install -qy jq gobject-introspection libgirepository-1.0-1 gir1.2-gtk-3.0 gir1.2-glib-2.0 gir1.2-gstreamer-1.0 gir1.2-poppler-0.18 python3-pip python3-setuptools python3-wheel libgirepository1.0-dev pip install pygobject pycairo .[build_sphinx] - name: Build env: poeditor_api_token: ${{ secrets.POEDITOR_API_TOKEN }} run: | ./scripts/poedit.sh contributors python setup.py build_sphinx tar czf pympress-docs.tar.gz -C build/sphinx/html/ . - name: Upload uses: actions/upload-artifact@v1 with: name: pympress-docs.tar.gz path: pympress-docs.tar.gz deploy: name: Deploy docs needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 with: ref: gh-pages - name: Clean run: find . -maxdepth 1 -not -name os-icons -not -name resources -exec echo rm -rf {} + - name: Download uses: actions/download-artifact@v1 with: name: pympress-docs.tar.gz path: . - name: Extract and push run: | tar xzf pympress-docs.tar.gz rm pympress-docs.tar.gz git add . git -c user.email=me@cimba.li -c user.name="${GITHUB_ACTOR}" commit -m "Github Action-built docs update" git push "https://Cimbali:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git" HEAD:gh-pages pympress-1.7.1/.github/workflows/draft_release.yml000066400000000000000000000151201415371354200223260ustar00rootroot00000000000000name: 'Draft release: build binaries etc.' on: # On new tags, build binaries and srpm, run a full brew test, and create a draft release automatically. create: # We often mess up the automatic build. Allow to correct manually (optionally with different build numbers) workflow_dispatch: inputs: build: description: 'build number' required: false default: 1 tag: description: 'Release tag for which to build' required: true jobs: macos: name: Install and run test on mac runs-on: macos-latest steps: - name: Configure brew repo continue-on-error: true run: | cd "`brew --repo homebrew/core`" # Credentials and remotes git remote add gh "https://github.com/Cimbali/homebrew-core/" git fetch gh # Attempt a rebase of changes in our repo copy git checkout --detach git rebase origin/master gh/master && git branch -f master HEAD || git rebase --abort # Now use master and update remote so we can use the bump-formula-pr git checkout master cd - name: Run the audit continue-on-error: true run: | brew audit --strict pympress - name: Install latest run: | brew install pympress --only-dependencies brew install pympress --build-from-source --HEAD - name: Test run: | brew test pympress srpm: name: Source RPM runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Install dependencies run: | sudo apt-get update -q sudo apt-get install -qy python3-rpm python3 -m pip install --upgrade pip python3 -m pip install setuptools wheel twine babel pysrpm rpmlint - name: Update translations env: poeditor_api_token: ${{ secrets.POEDITOR_API_TOKEN }} run: | ./scripts/poedit.sh download python3 setup.py compile_catalog - name: Build binaries env: BUILD_DIR: build/rpm run: | mkdir srpm build=${{ github.event.inputs.build }} pysrpm --dest-dir=srpm/ --source-only --release ${build:-1} . - name: Make tag name id: name run: | ref=${{ github.ref }} [ "${ref::10}" = 'refs/tags/' ] && tag=${ref:10} || tag=${{ github.event.inputs.tag }} echo ::set-output name=tag::$tag - name: Create GitHub Release and upload uses: softprops/action-gh-release@v1 with: draft: true tag_name: ${{ steps.name.outputs.tag }} fail_on_unmatched_files: true files: srpm/*.src.rpm env: GITHUB_TOKEN: ${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Check built RPM with rpmlint run: rpmlint srpm/*.src.rpm windows-build: name: Windows Binaries runs-on: windows-latest defaults: run: shell: msys2 {0} strategy: matrix: include: - { arch: x86_64, msystem: MINGW64 } - { arch: i686, msystem: MINGW32 } steps: - name: Checkout code uses: actions/checkout@v2 with: fetch-depth: 0 - name: Setup msys and dependencies uses: msys2/setup-msys2@v2 with: msystem: ${{ matrix.msystem }} update: true install: >- git zip base-devel mingw-w64-${{ matrix.arch }}-jq mingw-w64-${{ matrix.arch }}-curl mingw-w64-${{ matrix.arch }}-gtk3 mingw-w64-${{ matrix.arch }}-cairo mingw-w64-${{ matrix.arch }}-poppler mingw-w64-${{ matrix.arch }}-python3 mingw-w64-${{ matrix.arch }}-python3-pip mingw-w64-${{ matrix.arch }}-python3-gobject mingw-w64-${{ matrix.arch }}-python3-cairo mingw-w64-${{ matrix.arch }}-python3-appdirs mingw-w64-${{ matrix.arch }}-python3-setuptools mingw-w64-${{ matrix.arch }}-python3-packaging mingw-w64-${{ matrix.arch }}-python3-cx_Freeze mingw-w64-${{ matrix.arch }}-python3-babel mingw-w64-${{ matrix.arch }}-python3-watchdog - name: Install python-only dependencies run: | python3 -m pip install --disable-pip-version-check --upgrade pip python3 -m pip install python-vlc - name: Update translations env: poeditor_api_token: ${{ secrets.POEDITOR_API_TOKEN }} run: | ./scripts/poedit.sh download python3 setup.py compile_catalog - name: Build binary run: python3 setup.py --freeze build_exe - name: Make file and tag names id: name run: | ref=${{ github.ref }} [ "${ref::10}" = 'refs/tags/' ] && tag=${ref:10} || tag=${{ github.event.inputs.tag }} echo ::set-output name=file::pympress-$tag-${{ matrix.arch }} echo ::set-output name=tag::$tag - name: Build installer run: python3 setup.py --freeze bdist_msi --target-name ${{ steps.name.outputs.file }}.msi --skip-build - name: Make portable install run: | cd build mv exe.* pympress cp ../pympress/share/defaults.conf pympress/pympress.conf zip -r ../dist/${{ steps.name.outputs.file }}.zip pympress/ cd - - name: Install pympress shell: pwsh run: | $installer = gci -path dist\* -include *.msi -name Start-Process msiexec.exe -Wait -NoNewWindow -ArgumentList ('/i "dist\{0}" /qn /norestart /L* installer.log' -f $installer) echo "::group::Installer log" get-content installer.log echo "::endgroup::" - name: Run pympress shell: pwsh run: | # Check pympress install dir is appended to one of the $PATH variables $dir = ( [System.Environment]::GetEnvironmentVariable("Path","Machine").split(";") + [System.Environment]::GetEnvironmentVariable("Path","User").split(";") ) | Select-String 'pympress' gci -path $dir -filter *exe Start-Process "$dir\pympress.exe" -Wait -NoNewWindow -ArgumentList "--quit" echo "::group::Pympress log" get-content "$env:LOCALAPPDATA\pympress.log" echo "::endgroup::" - name: Create GitHub Release and upload uses: softprops/action-gh-release@v1 with: draft: true tag_name: ${{ steps.name.outputs.tag }} fail_on_unmatched_files: true files: | dist/*.zip dist/*.msi env: GITHUB_TOKEN: ${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} pympress-1.7.1/.github/workflows/lint_pr.yml000066400000000000000000000020531415371354200211760ustar00rootroot00000000000000name: Linting on PR, with stricter rules on new code on: [pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 strategy: matrix: python-version: [3.5, 3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flake8 - name: Install dependencies to lint docstrings if: matrix.python-version == 3.9 run: | python -m pip install flake8-docstrings - name: Fetch pull request run: | git fetch origin ${GITHUB_REF%/merge}/head - name: Lint changes with flake8 run: | # Reduced list of ignores, applied on the changed lines only git diff FETCH_HEAD -U0 | flake8 --diff --count --show-source --statistics --ignore=D107,D200,D210,D413,E251,E302,E303,W504 pympress-1.7.1/.github/workflows/lint_push.yml000066400000000000000000000020151415371354200215320ustar00rootroot00000000000000name: Linting on push on: [push] jobs: lint: runs-on: ubuntu-latest strategy: matrix: python-version: [3.5, 3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flake8 - name: Lint with flake8 run: | # Full list of ignores, fail on errors. No Docs errors. flake8 . --count --show-source --statistics --select=E,F,W,C - name: Install dependencies to lint docstrings if: matrix.python-version == 3.9 run: | python -m pip install flake8-docstrings - name: Lint docstrings if: matrix.python-version == 3.9 run: | flake8 . --count --show-source --statistics --select=D | sed -r 'h;s/^(\S+):([0-9]+):([0-9]+): /::warning file=\1,line=\2,col=\3::/p;g' pympress-1.7.1/.github/workflows/publish_release.yml000066400000000000000000000107561415371354200227060ustar00rootroot00000000000000name: 'Publish package: upload to pypi, brew, and copr' on: # When the draft release is converted to a public release, send out the binaries etc. to all the platforms release: types: [published] # Manual trigger workflow_dispatch: inputs: tag: description: 'Release tag for which to build' required: true skip_pypi: description: 'skip uploading to pypi' required: false default: no jobs: pypi: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine babel - name: Build catalogs and packages env: poeditor_api_token: ${{ secrets.POEDITOR_API_TOKEN }} run: | ./scripts/poedit.sh download python setup.py compile_catalog python setup.py sdist bdist_wheel - name: Upload env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} if: ${{ github.event_name == 'release' || github.event.inputs.skip_pypi == 'no' }} run: | twine upload dist/* copr: name: Download source RPM from release and upload to COPR runs-on: ubuntu-latest steps: - name: Download from GitHub Release run: | ref=${{ github.ref }} [ "${ref::10}" = 'refs/tags/' ] && tag=${ref:10} || tag=${{ github.event.inputs.tag }} curl -s -u "Cimbali:$GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json" -o release.json "https://api.github.com/repos/Cimbali/pympress/releases/tags/$tag" jq -r '[.assets[] | select(.name | endswith(".src.rpm"))][0]' release.json | tee srpm.json | jq 'del(.uploader)' curl -L "`jq -r .browser_download_url srpm.json`" -o "`jq -r .name srpm.json`" env: GITHUB_TOKEN: ${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Upload to COPR continue-on-error: true run: | trap 'rm -f ./copr-config' EXIT && echo "$COPR_TOKEN_CONFIG" > ./copr-config python3 -m pip install copr-cli copr-cli --config ./copr-config build --nowait cimbali/pympress python3-pympress-*.src.rpm env: COPR_TOKEN_CONFIG: ${{ secrets.COPR_TOKEN_CONFIG }} brew: name: Request new pypi package be pulled into Homebrew needs: pypi runs-on: macos-latest steps: - name: Define name id: name run: | ref=${{ github.ref }} [ "${ref::10}" = 'refs/tags/' ] && tag=${ref:10} || tag=${{ github.event.inputs.tag }} if echo ${tag#v} | grep -qxE '[0-9]+(\.[0-9]+)*' ; then release=final; else release=prerelease; fi echo ::set-output name=tag::${tag#v} echo ::set-output name=release::$release - name: Install dependencies run: | brew update brew install jq pipgrip - name: Wait a reasonable amount of time for pypi to update its info if: ${{ github.event_name == 'release' || github.event.inputs.skip_pypi == 'no' }} run: sleep 60 # be patient with pypi - name: Configure brew repo run: | cd "`brew --repo homebrew/core`" # Credentials and remotes git config user.name Cimbali git config user.email me@cimba.li git config credential.helper store echo -e "protocol=https\nhost=github.com\nusername=Cimbali\npassword=$PASSWORD" | git credential-store store git remote add gh "https://github.com/Cimbali/homebrew-core/" git fetch gh # Attempt a rebase of changes in our repo copy git checkout --detach git rebase origin/master gh/master && git branch -f master HEAD || git rebase --abort # Now use master and update remote so we can use the bump-formula-pr git checkout master git push gh -f master:master env: PASSWORD: ${{ secrets.GITHUB_HOMEBREW_TOKEN }} - name: Make a brew PR from pypi’s metadata if: ${{ steps.name.outputs.release == 'final' }} run: | jq_script=`printf '.releases."%s"[]? | select(.python_version == "source") | @text "--url=\(.url) --sha256=\(.digests.sha256)"' ${{ steps.name.outputs.tag }}` brew bump-formula-pr --strict --no-browse `curl -s https://pypi.org/pypi/pympress/json | jq -r "$jq_script"` pympress env: HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_HOMEBREW_TOKEN }} HUB_REMOTE: https://github.com/Cimbali/homebrew-core/ pympress-1.7.1/.gitignore000066400000000000000000000001301415371354200153710ustar00rootroot00000000000000*.pyc build/ dist/ pympress.egg-info/ pympress/share/locale/*/LC_MESSAGES/pympress.mo pympress-1.7.1/LICENSE.txt000066400000000000000000000432541415371354200152420ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. pympress-1.7.1/README.md000066400000000000000000000630271415371354200146760ustar00rootroot00000000000000# ![Pympress logo](https://raw.githubusercontent.com/Cimbali/pympress/master/pympress/share/pixmaps/pympress-32.png) What is Pympress? Pympress is a PDF presentation tool designed for dual-screen setups such as presentations and public talks. Highly configurable, fully-featured, and portable It comes with many great features ([more below](#functionalities)): - supports embedded gifs (out of the box), videos, and audios (with VLC or Gstreamer integration) - text annotations displayed in the presenter window - natively supports beamer's *notes on second screen*, as well as Libreoffice notes pages! Pympress is a free software, distributed under the terms of the GPL license (version 2 or, at your option, any later version). Pympress was originally created and maintained by [Schnouki](https://github.com/Schnouki), on [his repo](https://github.com/Schnouki/pympress). Here is what the 2 screen setup looks like, with a big notes slide next to 2 small slides (current and next) on the presenter side: ![A screenshot with Pympress’ 2 screens](https://cimbali.github.io/pympress/resources/pympress-screenshot.png) # Installing [![github version badge][github_version]][github_release] - Ubuntu ![ubuntu logo][ubuntu] 20.04 focal or newer, Debian ![debian logo][debian] 11 Bullseye or newer [![ubuntu version badge][ubuntu_version]][ubuntu_package] [![debian version badge][debian_version]][debian_package] (maintained by [@mans0954](https://github.com/mans0954)) apt-get install pympress libgtk-3-0 libpoppler-glib8 libcairo2 python3-gi python3-gi-cairo gobject-introspection libgirepository-1.0-1 gir1.2-gtk-3.0 gir1.2-poppler-0.18 - RPM-based Linux (Fedora ![fedora logo][fedora] CentOS ![centos logo][centos] Mageia ![mageia logo][mageia] OpenSuse ![suse logo][suse] RHEL) [![Copr build version][copr_build_version]][copr_package] You can get pympress from the [pympress COPR repo][copr_repo] of your system. With yum or dnf, simply do: ```sh dnf copr enable cimbali/pympress dnf install python3-pympress ``` With zypper, fetch the link of the .repo in the table at the bottom of the COPR page and add it as a source. ```sh zypper addrepo https://copr.fedorainfracloud.org/coprs/cimbali/pympress/repo/opensuse-tumbleweed/cimbali-pympress-opensuse-tumbleweed.repo zypper install python3-pympress ``` - Arch Linux ![arch linux logo][arch_linux] from AUR [![AUR version badge][aur_version]][aur_package] (maintained by [@Jose1711](https://github.com/jose1711)) ```sh git clone https://aur.archlinux.org/python-pympress.git cd python-pympress makepkg -si pacman -S poppler-glib # dependency temporarily missing from AUR package ``` Or using any other tool to manage AUR packages (yay, pacaur, etc.): ```sh yay -S python-pympress pacman -S poppler-glib # dependency temporarily missing from AUR package ``` - macOS ![apple logo][apple] using [Homebrew](https://brew.sh/) ![homebrew version badge][homebrew_version] ```sh brew install pympress ``` - Windows ![windows logo][windows] with [Chocolatey](https://chocolatey.org/) [![chocolatey version badge][chocolatey_version]][chocolatey_package] (maintained by [@ComFreek](https://github.com/ComFreek)) ```batch choco install pympress ``` Or download the latest installer from the [latest Github release][github_release].
Troubleshooting - If you get an error message along the lines of "MSVCP100.dll is missing", get the Visual C++ 2010 redistributables from Microsoft ([x86 (32 bit)](https://www.microsoft.com/en-in/download/details.aspx?id=5555) or [x64 (64 bits)](https://www.microsoft.com/en-us/download/details.aspx?id=14632)). Those libraries really should already be installed on your system.
- Other systems, directly from PyPI ![pypi version badge][pypi_version] − requires [python, gtk+3, poppler, and their python bindings](#dependencies): ``` python3 -m pip install "pympress" ```
Troubleshooting - Make sure you have all [the dependencies](#dependencies). (These are already included in binary packages or their dependencies.) - Using pip, you may want to install with the `--user` option, or install from github or downloaded sources. See [the python documentation on installing](https://docs.python.org/3.7/installing/index.html). - If your python environment lacks the Gobject Introspections module, try 1. using `--system-site-packages` for [virtual environments](https://docs.python.org/3.7/library/venv.html), 2. installing pygobject from pip (`pip install pygobject`, which requires the correct development/header packages. See [the PyPI installation instructions of PyGObject for your system](https://pygobject.readthedocs.io/en/latest/getting_started.html)).
[ubuntu]: https://cimbali.github.io/pympress/os-icons/ubuntu.png [debian]: https://cimbali.github.io/pympress/os-icons/debian.png [centos]: https://cimbali.github.io/pympress/os-icons/centos.png [windows]: https://cimbali.github.io/pympress/os-icons/windows-10.png [suse]: https://cimbali.github.io/pympress/os-icons/suse.png [linux]: https://cimbali.github.io/pympress/os-icons/linux.png [fedora]: https://cimbali.github.io/pympress/os-icons/fedora.png [mageia]: https://cimbali.github.io/pympress/os-icons/mageia.png [arch_linux]: https://cimbali.github.io/pympress/os-icons/archlinux.png [apple]: https://cimbali.github.io/pympress/os-icons/apple.png [ubuntu_package]: https://packages.ubuntu.com/focal/pympress [debian_package]: https://packages.debian.org/bullseye/pympress [copr_package]: https://copr.fedorainfracloud.org/coprs/cimbali/pympress/package/python3-pympress/ [copr_repo]: https://copr.fedorainfracloud.org/coprs/cimbali/pympress/ [aur_package]: https://aur.archlinux.org/packages/python-pympress/ [chocolatey_package]: https://chocolatey.org/packages/pympress [github_release]: https://github.com/Cimbali/pympress/releases/latest [copr_build_version]: https://img.shields.io/badge/dynamic/json?label=COPR%20build&query=%24.builds%5B0%5D.build.package_version&url=https%3A%2F%2Fcopr.fedorainfracloud.org%2F%2Fapi_2%2Fbuilds%3Fproject_id%3D29551%26limit%3D1&prefix=v&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNC4zNzU0MjNtbSIgaGVpZ2h0PSIzNC40MTYyMjltbSIgdmlld0JveD0iMCAwIDM0LjM3NTQyMyAzNC40MTYyMjkiIHZlcnNpb249IjEuMSI+CjxlbGxpcHNlIHN0eWxlPSJmaWxsOiNkMzhkNWY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuODUyMDg7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1vcGFjaXR5OjEiIGN4PSIyMy45NTYwNDMiIGN5PSI0LjE1NzQwMiIgcng9IjE3LjIxMTEwMiIgcnk9IjE3LjE4NDcyMSIgdHJhbnNmb3JtPSJtYXRyaXgoMC44MTcyMjcxOSwwLjU3NjMxNTY0LC0wLjU3NDgzNTQ1LDAuODE4MjY5MDMsMCwwKSIgLz4KPGVsbGlwc2Ugc3R5bGU9ImZpbGw6Izc4NDQyMTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MS44NTIwODtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLW9wYWNpdHk6MSIgY3g9IjIzLjkwODM5IiBjeT0iNC4yOTczMjI4IiByeD0iMTIuNzYyMTE2IiByeT0iMTIuNzQzNTE3IiB0cmFuc2Zvcm09Im1hdHJpeCgwLjgyMDU3NzU0LDAuNTcxNTM1MjEsLTAuNTY1NjkzODMsMC44MjQ2MTUzNiwwLDApIiAvPgo8cGF0aCBzdHlsZT0iZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojZDM4ZDVmO3N0cm9rZS13aWR0aDoyLjMwNTE4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1vcGFjaXR5OjEiIGQ9Im0gMjYuMTg3Nzg3LDUuMDc3Mzc3MyBjIDAsMCAtMTguNzQxMzc4NiwzLjI5MzI5OCAtMTcuMzE1MDI3OSwyNC4zMDM2MDg3IiAvPgo8cGF0aCBzdHlsZT0iZmlsbDpub25lO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTojZDM4ZDVmO3N0cm9rZS13aWR0aDoyLjMwNTE4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1vcGFjaXR5OjEiIGQ9Im0gMjUuNzE0NTY0LDQuNDQ0OTkwOCBjIDAsMCAzLjAyOTU5MywxOC43NTk5NzMyIC0xNy4zMTUwMjM3LDI0LjMwMzYwNDIiIC8+Cjwvc3ZnPgo= [pypi_version]: https://img.shields.io/pypi/v/pympress?logo=pypi&logoColor=yellow [aur_version]: https://img.shields.io/aur/version/python-pympress?logo=arch%20linux [homebrew_version]: https://img.shields.io/homebrew/v/pympress?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAHwHpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjarZdpluQqDoX/s4peghmEYDmM57wd9PL7k+2IyrEyqvqlM4yNMYh7pSvZrf/+s91/+As5ZpdES645H/ylmmpoXJTj+qvn2R/pPJ9/4X7E/bt+93wQ6Iq08brVdo9v9MuvFx5r+P6+35X7SSj3RP54t3S0le16vjWS/nD1+3RPVNd1kWvRt6b2e6JxDzxNuX/padbV2L1716GgNIWFYggr+nic53JZEK9f45c4++gZ52M+r7OjkfiwBEDebe/RHsdbgN6B/LhyH9F/Xn0AP7S7P37AMt8YcfHlAy8f+uNzmfB24fi0KLx/wAT103bu396z7L2u3bWUQTTfHnWC7R/TMLADeTxfyxzKT7jW86gc5WjHgPJ5jKNzDF99gJXtfPLTN7/9OtvhByamsILShjBCPPtK1FDDiMZTssPvoLHGGQtcjrBcjHSHpy3+XLee6w1fWHl6hgbPZJ5Xvj3c7x7+yeH2HgaRP8oTK+wK5teYYczZmVEQ4vfNm5wAP46b/uON/+CqMCgnzIUNtqNfU3Txv3wrnjxHxgntFULe6bwnACLWFozB+ZM/so/isz80BPUeHAsENSwPMYUOA14kTIwMKcYcnIYSbG3eUX+ODRJysG60CSKEaFK4qbFBVkqC/2gq+FCTKElEsqgUJ1UaApey5Jw1m8g1jZpUNKtq0aqtxJKKlFy0lFJLq6FGNFBqrlpLrbW14BoLNeZqjG/09NBjT1167tpLr70N3GekISMPHWXU0WaYcSITM0+dZdbZlncLpVhpycpLV1l1tY2v7bjTlp237rLrbk/WblY/HX/Amr9ZCydTNk6frNHrVB9TeJMTMc5gLCQP42oM4NDBODuKTykYc8bZUQNBIQEjxbhx0xtjUJiWD7L9k7tfzL3Em5PyEm/hJ+acUfdvMOeg7jNvX7A2Lc+Nk7ErCg3TIxJ9PF+luVCaJbX223a2NrQxM7/W7F1P/xZb01/jXpvohfZFizCn9dVXrFwcuXi/qpd1VNkpTEU1SUc7etHeBXmGqHR4e44MJAGRds51/Ny6Vwd+avuE1GaIldBbd0ZaJ+xyiHjAPsCP57uuhXAbonm11Vo54srK66swKhA8x8r5DQLu38C6gp7Dt0ZoO+EnCVj4RyxP/2QHIeDTbYnVLnI+pc1Jcp9cgq5MJda2BHE6OnGTrzms3PnLltKP6D0hOCOhTSAQc2ODa83ce39gssCk+XTimbgeBEq0HfYI2IT03C0dgXckrs5WDotV2YGYnoQlJVIaSWu6NveN9e4Vs2VtEWLUosTAan0+8IozD1REUEgdk1VHlNaFK8rQukXrwhwcc0zrwWVX5YZOlIgJ0JhgU1ExRMNidbdtTR1MtOKUrexnEPqYs/7ICdyvjo3jQWvuS1LbKobvRKDGRHz+ZCJODfM7dbAioZu0q12nji1v3eib1v0wwG+UWIPIWiIjTa2zFuindAurap97WznUI2CnPtFodDZM6WrQVd6ZaOLMu3skkoIl5t2K7DV1UZocaRp1MuoUaanHst1CX6NNGzdCvVmi6B4+MjtzRuKbJNFj7lY/DqWC9JaCWELukXNkeHaLSFlcrOGXli4Le7D5FK1hvG5SAZmCFCChyZphN+y4YizRso82/HRjrIQgj9z5TiI1kKZqwLdiuqAu5bWIc18+0IkvG7SEzRA+xLDHtmb5QptU1Ylk6Wp5zkw+rDM7HBBLtfkpqbeaLmfYJN71kMNYL50hY/WyTLQHORDAmuY9E+Wtn8ltymLttlInmKqfQTJMrzxjV/Z40htABPpwVQCyYEE2GxYUwm+msTyZdmEFBBD9XhodWwmoAjgKs8vkJc5XhNx98SCunZHyRmY5m2FetcZtXZxh9I5Djb4pL1aPoSeClmZYjB1m7Eztwltro9756O3Yfrn7HNBZgQSEreWeyn9ZETL2NQPFSNaJwFDNQALV6GGfFz9FmpxS+1WEaRel/iVMrCFl5b1zXLobOTbL3PlCnpgC+6biaJYarJzCanIB1fFIVPcb4UC9+wCXidNbyz310aaCY6lyTZDsppMDfAO6aaUGUfvXRQSxm6mwyCLBmu2p4EwI4p7HTj0sC9mAM85+55LhBl7ccFxOhGooJ0r4Y53ju8hSRZcDcW1iT7s1puE2hTvbG9cEp0sS/7nMrHY2Pb8z0e9j130TsxasxGw9mxdiNrufY/aT43eqLR+pU66ygVv8ED0auzQS8Ka8rdloQ6pyIYN1k1LbW9H/Q4/uvSkV8c96VJ56hNLeDhlec8g2p2W/0N5tTYaVYoc8HHIBAMV1H3wHvJpq3dfuGKwaJGezsFrx4Mm5doy8k30OxG5JrS48g3HAyxckOhCxiDBEbkxxSgAGP9Q0aF+pahmbi48LAgcpmG+SUZbSOv5MrLGhbAmJpHrsta8MiILBI77oi+UTGMwkgkKyIlb0niTCApmGrxvfHJqz+KwZAhOIeufDcpOHqUjIbIPayySFMtwSrn2MUFZuxm2ifB+kAD7t4qBIIkTWVeXm8BfF9nvNjrBOoPONENKt2amhWFiAz/fTr8Q2iIzSwYBpi++jUP3CjUlQdwvfWtBs1Wg9Xf5ZLunq46CaH6+Y5Z4RtPkmq9Q5/wPyUC09equEngAAAYVpQ0NQSUNDIHByb2ZpbGUAAHicfZE9SMNAHMVf04qi9QPaQcQhQ3WyICriqFUoQoVSK7TqYHLpFzRpSFJcHAXXgoMfi1UHF2ddHVwFQfADxMnRSdFFSvxfUmgR48FxP97de9y9A4R6malmYBxQNctIxWNiJrsqdr4igB70YQAhiZn6XDKZgOf4uoePr3dRnuV97s/Rq+RMBvhE4lmmGxbxBvH0pqVz3icOs6KkEJ8Tjxl0QeJHrssuv3EuOCzwzLCRTs0Th4nFQhvLbcyKhko8RRxRVI3yhYzLCuctzmq5ypr35C8M5rSVZa7THEYci1hCEiJkVFFCGRaitGqkmEjRfszDP+T4k+SSyVUCI8cCKlAhOX7wP/jdrZmfnHCTgjGg48W2P0aAzl2gUbPt72PbbpwA/mfgSmv5K3Vg5pP0WkuLHAH928DFdUuT94DLHWDwSZcMyZH8NIV8Hng/o2/KAqFboHvN7a25j9MHIE1dJW6Ag0NgtEDZ6x7v7mrv7d8zzf5+AAoZcn1+pfnLAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AQLDhA3m2FIoQAABGlJREFUWMPVl2tsk1UYx3/v29F37UrZJbvfuq6DXbBjhu2T4gclMUYZMXEkKoQtJpoAA2SEZBgjFxMx8QOKZgqGjtgRIEKyyZwyEJRlYmSJGZHdWHBL3Lpb1+1tS3c7fmDMoGx76YbE/7dzzpPn/M9z+ec5oBEJCQkfyrK8GwjlcWBzcfHF7+vrxcZNm5qAmOntPMDwnxCIiY39oKGxUTRe+0W8tG5dHVCwe88ev6IoXyzEr06rodfrbUlJSX3DbrcrS81LbaGhoc8nJSbp6+svbAYGHzkBYPTy5R/6V2SueNFmy5BkWV525JOPK1RVrQLigHFg8mEJhGi027B1W+n+7Jzs5cPuYc6cPk2qJZWenp7sbaXbW7NzsjMGBwbVE5WOI21tbe9Ok9EEaT4DRVHeOnrsy89yV62asRVCMDExQSAQwGQyzdi6XC7eP3Cg6sqVy68tVgri9h88+M2aNc/o72MtSeh0OvT6+7YxmUwkJiY+ce7s2SvAbS0E5HnOX01NSQ2rPX+eb2tr6e/vf6CR2+2mu7ubsbEAmVlZJCQkvKw1AnMSMBrD7IE7fna9vZOdO7bT2dHOyIiHYbebwcG7hX/16k94R0dIjI+jsaEBj8fDU0+vWb4Yrb++vLzc5fF4xD309PSIog0bRF1dnbh06ZIo37tX1NTUzJyPj4+LQ4cOCYfDMVZcXFwLhAd7eURlZaVP/AN9fX3i+vXrM+u2tjbR0dEhHoSBgQFhMpn2BduG4fHx8f+S2OjoaKKjo2fWGRkZszo2GAwYjUaTqqpBdcGw3+/PslgsKw0GAwaDdrn3er00NzfjdDq7ampqtgDuYHVABo5WV1eXuJqqkWPtxCRZiYqKxGw2oygKkiRhsVjQ6e6+49zXZ5ga81H/48/tFRUVK4GxhSjhFDD03Nq1LNGfQja24On7Dm/rEKP+SUbHJnGr4zTnvEdh4XoCgQCj146w8Z0qUmzZVrvdfgugtbX1j8OHD+8Afl2IFCPHrGCZECwN8RIX8fd+w+/76MhZidlsJjl8GIDVq1frcnNzkyRJQpblpOTk5OqysjIrcOdhhUgg5k5YXrqZ8199yqkTx7DFhwFw8uRJj6IoH+n1+s+bmpooKCiIB9IWFIFZxUrRsfXJ3xACJEkBoKurqxsoA8J7e3vfjIiImLXg54/AfSGYo5ql4B4wHwGN12uCCDICjxYyPF4K/4MIBMdBN+17iTRPdc7XhiKY6wsLC7PS0tLckiTp8vPzuXHjBoDvkejAg5CZmYnNZjPfmxOrqqouzDaihWjOwNQkYlLbsOtwOIZKSkoc09p5Gzg2Wy5DtAiRFFnAxK1amAzMLUYxLyCFRuByuf4Edi3av0CXXwr+1xFDtxHefggMIsZ9oFuCFBoFYbHIkVYwRs2M7YuFV5xOp7+9vV2oqirmgqqq4ubNm+L48ePD6enpWxbtYwJEAs8CeUVFRTar1RpnNpvDZFkOEUJM+Xw+X2dnZ6/T6WwBrgEXAa9WAn8BvCj26liVnwYAAAAASUVORK5CYII= [ubuntu_version]: https://img.shields.io/ubuntu/v/pympress?logo=ubuntu [debian_version]: https://img.shields.io/debian/v/pympress/stable?logo=debian [chocolatey_version]: https://img.shields.io/chocolatey/v/pympress?logo=chocolatey [github_version]: https://img.shields.io/github/v/release/Cimbali/pympress?label=Latest%20GitHub%20release&logo=github ### Notes To support playing embedded videos in the PDFs, your system must have VLC installed (with the same bitness as pympress). VLC is not distributed with pympress, but it is certainly available in your system’s package manager and [on their website](https://www.videolan.org/vlc/). # Usage ## Opening a file Simply start Pympress and it will ask you what file you want to open. You can also start pympress from the command line with a file to open like so: `pympress slides.pdf` or `python3 -m pympress slides.pdf` ## Functionalities All functionalities are available from the menus of the window with slide previews. Don't be afraid to experiment with them! Keyboard shortcuts are also listed in these menus. Some more usual shortcuts are often available, for example `Ctrl`+`L`, and `F11` also toggle fullscreen, though the main shortcut is just `F`. A few of the fancier functionalities are listed here: - **Two-screen display**: See on your laptop or tablet display the current slide, the next slide, the talk time and wall-clock time, and annotations (either PDF annotations, beamer notes on second slide, or Libreoffice notes pages). The position of the beamer or Libreoffice notes in the slide is detected automatically and can be overridden via a menu option. - **Media support**: supports playing video, audio, and gif files embedded in (or linked from) the PDF file. - **Highlight mode**: Allows one to draw freehand on the slide currently on screen. - **Go To Slide**: To jump to a selected slide without flashing through the whole presentation on the projector, press `G` or click the "current slide" box. Using `J` or clicking the slide label will allow you to navigate slide labels instead of page numbers, useful e.g. for multi-page slides from beamer `\pause`. A spin box will appear, and you will be able to navigate through your slides in the presenter window only by scrolling your mouse, with the `Home`/`Up`/`Down`/`End` keys, with the + and - buttons of the spin box, or simply by typing in the number of the slide. Press `Enter` to validate going to the new slide or `Esc` to cancel. - **Software pointer**: Clicking on the slide (in either window) while holding `ctrl` down will display a software laser pointer on the slide. Or press `L` to permanently switch on the laser pointer. - **Talk time breakdown**: The `Presentation > Timing Breakdown` menu item displays a breakdown of how much time was spent on each slide, with a hierarchical breakdown per chapters/sections/etc. if available in the PDF. - **Automatic file reloading**: If the file is modified, pympress will reload it (and preserve the current slide, current time, etc.) - **Big button mode**: Add big buttons (duh) for touch displays. - **Swap screens**: If Pympress mixed up which screen is the projector and which is not, press `S` - **Estimated talk time**: Click the `Time estimation` box and set your planned talk duration. The color will allow you to see at a glance how much time you have left. - **Adjust screen centering**: If your slides' form factor doesn't fit the projectors' and you don't want the slide centered in the window, use the "Screen Center" option in the "Presentation" menu. - **Resize Current/Next slide**: You can drag the bar between both slides on the Presenter window to adjust their relative sizes to your liking. - **Preferences**: Some of your choices are saved in a configuration file, and more options are accessible there. See the [configuration file documentation](docs/options.md) for more details. - **Caching**: For efficiency, Pympress caches rendered pages (up to 200 by default). If this is too memory consuming for you, you can change this number in the configuration file. - **Configurability**: Layout of presenter window dynamically configurable, with 1 to 16 next slides preview - **Editable PDF annotations**: Annotations can be added, removed, or changed, and the modified PDF files can be saved - **Automatic next slide and looping** ## Command line arguments - `-h, --help`: Shows a list of all command line arguments. - `-t mm[:ss], --talk-time=mm[:ss]`: The estimated (intended) talk time in minutes and optionally seconds. - `-n position, --notes=position`: Set the position of notes on the pdf page (none, left, right, top, or bottom). Overrides the detection from the file. - `--log=level`: Set level of verbosity in log file (DEBUG, INFO, WARNING, ERROR). ## Media and autoplay To enable media playback, you need to have either: - Gstreamer installed (enabled by default), with plugins gstreamer-good/-bad/-ugly based on which codecs you need, or - VLC installed (and the python-vlc module), with `enabled = on` under the `[vlc]` section of your config file. To produce PDFs with media inclusion, the ideal method is to use beamer’s multimedia package, always with `\movie`: ```latex \documentclass{beamer} \usepackage{multimedia} \begin{frame}{Just a mp4 here} \centering \movie[width=0.3\textwidth]{\includegraphics[width=0.9\textwidth]{frame1.png}}{movie.mp4} \movie[width=0.3\textwidth]{}{animation.gif} \movie[width=0.3\textwidth]{}{ding.ogg} \end{frame} ``` If you desire autoplay, ensure you have pympress ≥ 1.7.0 and poppler ≥ 21.04, and use the `movie15` package as follows: ```latex \documentclass{beamer} \usepackage{movie15} \begin{document} \begin{frame} \begin{center} \includemovie[attach=false,autoplay,text={% \includegraphics{files/mailto.png}% }]{0.4\linewidth}{0.3\linewidth}{files/random.mpg} \end{center} \end{frame} \end{document} ``` # Dependencies Pympress relies on: * Python (version ≥ 3.4, python 2.7 is supported only until pympress 1.5.1, and 3.x < 3.4 until v1.6.4). * [Poppler](http://poppler.freedesktop.org/), the PDF rendering library. * [Gtk+ 3](http://www.gtk.org/), a toolkit for creating graphical user interfaces, and [its dependencies](https://www.gtk.org/overview.php), specifically: * [Cairo](https://www.cairographics.org/) (and python bindings for cairo), the graphics library which is used to pre-render and draw over PDF pages. * Gdk, a lower-level graphics library to handle icons. * [PyGi, the python bindings for Gtk+3](https://wiki.gnome.org/Projects/PyGObject). PyGi is also known as *pygobject3*, just *pygobject* or *python3-gi*. * Introspection bindings for poppler may be shipped separately, ensure you have those as well (`typelib-1_0-Poppler-0_18` on OpenSUSE, `gir1.2-poppler-0.18` on Ubuntu) * optionally [VLC](https://www.videolan.org/vlc/), to play videos (with the same bitness as Python) and the [python-vlc](https://pypi.org/project/python-vlc/) bindings. * optionally Gstreamer to play videos (which is a Gtk library) ### On linux platforms The dependencies are often installed by default, or easily available through your package or software manager. For example, on ubuntu, you can run the following as root to make sure you have all the prerequisites *assuming you use python3*: ```sh apt-get install python3 python3-pip libgtk-3-0 libpoppler-glib8 libcairo2 python3-gi python3-cairo python3-gi-cairo gobject-introspection libgirepository-1.0-1 libgirepository1.0-dev gir1.2-gtk-3.0 gir1.2-poppler-0.18 ``` Different distributions might have different package naming conventions, for example the equivalent on OpenSUSE would be: ```sh zypper install python3 python3-pip libgtk-3-0 libpoppler-glib8 libcairo2 python3-gobject python3-gobject-Gdk python3-cairo python3-gobject-cairo typelib-1_0-GdkPixbuf-2_0 typelib-1_0-Gtk-3_0 typelib-1_0-Poppler-0_18 ``` On CentOS/RHEL/Fedora the dependencies would be: ```sh yum install python36 python3-pip gtk3 poppler-glib cairo gdk-pixbuf2 python3-gobject python3-cairo ``` And on Arch Linux: ```sh pacman -S --needed python python-pip gtk3 poppler cairo gobject-introspection poppler-glib python-gobject ``` ### On macOS Dependencies can be installed using [Homebrew](https://brew.sh/): ```sh brew install --only-dependencies pympress ``` ### On windows The [binary installer for windows](#installing) comes with pympress and all its dependencies packaged. Alternately, in order to install from pypi or from source on windows, there are two ways to get the dependencies: 1. using MSYS2 (replace x86_64 with i686 if you're using a 32 bit machine). **Warning:** this can take a substantial amount of disk size as it requires a full software distribution and building platform. ```sh pacman -S --needed mingw-w64-x86_64-gtk3 mingw-w64-x86_64-cairo mingw-w64-x86_64-poppler mingw-w64-x86_64-python3 mingw-w64-x86_64-vlc python3-pip mingw-w64-x86_64-python3-pip mingw-w64-x86_64-python3-gobject mingw-w64-x86_64-python3-cairo ``` This is also the strategy used to automate [builds on appveyor](https://github.com/Cimbali/pympress/tree/master/scripts/build_msi_mingw.sh). 2. Using PyGobjectWin32. *Be sure to check the supported Python versions (up to 3.4 at the time of writing)*, they appear in the FEATURES list in the linked page. - Install native [python for windows](https://www.python.org/downloads/windows/) - Get GTK+3, Poppler and their python bindings by executing [the PyGi installer](https://sourceforge.net/projects/pygobjectwin32/). Be sure to tick all the necessary dependencies in the installer (Poppler, Cairo, Gdk-Pixbuf). Alternately, you can build your Gtk+3 stack from source using MSVC, see [the Gnome wiki](https://wiki.gnome.org/Projects/GTK+/Win32/MSVCCompilationOfGTKStack) and [this python script that compiles the whole Gtk+3 stack](https://github.com/wingtk/gvsbuild/). This strategy has not been used successfully yet, due to problems building Poppler with its introspection bidings (i.e. typelib) − see [#109](https://github.com/Cimbali/pympress/issues/109). # Contributing Feel free to clone this repo and use it, modify it, redistribute it, etc, under the GPLv2+. A [number of contributors](https://github.com/Cimbali/pympress/graphs/contributors) have taken part in the development of pympress and submitted pull requests to improve it. **Be respectful of everyone and keep this community friendly, welcoming, and harrasment-free. Abusive behaviour will not be tolerated, and can be reported by email at me@cimba.li − wrongdoers may be permanently banned.** Pympress has inline sphinx documentation ([Google style](http://www.sphinx-doc.org/en/latest/ext/example_google.html), contains rst syntax), and the [docs generated from it are hosted on the github pages of this repo](https://cimbali.github.io/pympress/). ## Translations ![Czech](https://img.shields.io/poeditor/progress/301055/cs?token=7a666b44c0985d16a7b59748f488275c&label=%F0%9F%87%A8%F0%9F%87%BF%20Czech) ![French](https://img.shields.io/poeditor/progress/301055/fr?token=7a666b44c0985d16a7b59748f488275c&label=%F0%9F%87%AB%F0%9F%87%B7%20French) ![German](https://img.shields.io/poeditor/progress/301055/de?token=7a666b44c0985d16a7b59748f488275c&label=%F0%9F%87%A9%F0%9F%87%AA%20German) ![Polish](https://img.shields.io/poeditor/progress/301055/pl?token=7a666b44c0985d16a7b59748f488275c&label=%F0%9F%87%B5%F0%9F%87%B1%20Polish) ![Spanish](https://img.shields.io/poeditor/progress/301055/es?token=7a666b44c0985d16a7b59748f488275c&label=%F0%9F%87%AA%F0%9F%87%B8%20Spanish) ![Italian](https://img.shields.io/poeditor/progress/301055/it?token=7a666b44c0985d16a7b59748f488275c&label=%F0%9F%87%AE%F0%9F%87%B9%20Italian) If you want to add or contribute to a translation, check [pympress’ page on POEditor](https://poeditor.com/join/project/nKfRxeN8pS) and add your efforts to make pympress available in your own language to those of [@Vulpeculus](https://github.com/Vulpeculus), [@polaksta](https://github.com/polaksta), [@susobaco](https://github.com/susobaco), Agnieszka, Ferdinand Fichtner, [FriedrichFröbel](https://github.com/FriedrichFroebel), [Jaroslav Svoboda](https://github.com/multiflexi), Jeertmans, Kristýna, Lorenzo. pacchiardi, Luis Sibaja, Nico, Saulpierotti, and Cimbali. ## Packages Official releases are made to [PyPI](https://pypi.org/) and with [github releases](https://github.com/Cimbali/pympress/releases). The community maintains a number of other packages or recipes to install pympress (see [Install section](#installing-)). Any additions welcome. pympress-1.7.1/docs/000077500000000000000000000000001415371354200143375ustar00rootroot00000000000000pympress-1.7.1/docs/_template/000077500000000000000000000000001415371354200163115ustar00rootroot00000000000000pympress-1.7.1/docs/_template/breadcrumbs.html000066400000000000000000000005461415371354200214750ustar00rootroot00000000000000{% extends "!breadcrumbs.html" %} {# do the setup for the github links to appear correctly #} {% set display_github = True %} {% set github_user = 'Cimbali' %} {% set github_repo = 'pympress' %} {% set github_version = 'master' %} {% if pagename == 'README' %} {% set conf_py_path = '/' %} {% else %} {% set conf_py_path = '/docs/source/' %} {% endif %} pympress-1.7.1/docs/_template/layout.html000066400000000000000000000013171415371354200205160ustar00rootroot00000000000000{% extends "!layout.html" %} {% block sidebartitle %} Pympress on GitHub
Docs home
v{{ version }}
{% include "searchbox.html" %} {% endblock %} {% block menu %} {{ super() }}
Index Module index {% endblock %} pympress-1.7.1/docs/conf.py000066400000000000000000000410411415371354200156360ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. """ pympress documentation build configuration file for sphinx_build. """ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use the absolute path. import sys import pathlib sys.path.insert(0, pathlib.Path(__file__).resolve().parents[1]) from recommonmark.transform import AutoStructify import re import subprocess import importlib from urllib.parse import urlsplit, urlunsplit, urljoin from urllib.request import url2pathname # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.3' # for sphinx.ext.napoleon # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'recommonmark', 'sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.doctest', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_template'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.md'] github_doc_root = 'https://cimbali.github.io/pympress/' def rewrite_link(url): """ Make relative links in README relative to "docs/" or absolute. """ split_url = urlsplit(url) if split_url.netloc or not split_url.path: return url elif split_url.path.startswith('docs/'): return urlunsplit(split_url._replace(path = split_url.path[5:])) else: return urljoin(github_doc_root, url) def setup(app): """ Function called by sphinx to setup this documentation. """ app.add_config_value('recommonmark_config', { 'url_resolver': lambda url: url[3:] if url.startswith('../README') else url, 'enable_auto_toc_tree': True, 'auto_toc_maxdepth': 2, 'auto_toc_tree_section': 'Contents', 'enable_eval_rst': True }, True) app.add_transform(AutoStructify) # get the README.md as a source, but we need to move it here and adjust the relative links into docs/ # Until relative links are allowed from the toctree, see https://github.com/sphinx-doc/sphinx/issues/701 find_links = re.compile(r'\[([^\[\]]+)\]\(([^()]+)\)') here = pathlib.Path(__file__).parent with open(here.parent.joinpath('README.md')) as fin, open(here.joinpath('README.md'), 'w') as fout: for line in fin: print(find_links.sub(lambda m: '[{}]({})'.format(m[1], rewrite_link(m[2])), line), end='', file=fout) app.connect('build-finished', lambda app, config: pathlib.Path(app.srcdir).joinpath('README.md').unlink()) # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. Make sure we find the right omdule info. pkg_meta = importlib.import_module('pympress.__init__') project = 'pympress' copyright = '2009-2011, Thomas Jost; 2015-2020 Cimbali' author = 'Thomas Jost, Cimbali' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = pkg_meta.__version__ # The full version, including alpha/beta/rc tags. try: release = str(subprocess.check_output(["git", "describe"])[1:].strip()) except Exception: release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%d %B, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = 'obj' # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True def load_epydoc_as_intersphinx_v2(url): """ Get an epydoc objects list from an URL and convert it to intershphinx v2 format. Arguments: url (`str`): the URL where the documentation is available. In particular url + '/api-objects.txt' must contain the list of object generated by epydox. Returns: a (url, filename) tuple where the file contains the intersphinx list of objects """ import codecs import requests import tempfile def guess_epydoc_role(name, uri): uri = urlsplit(uri) # filenames in URI are name-module.html or name-class.html base = pathlib.Path(url2pathname(uri.path)).stem.split('-')[-1] if not uri.fragment: return base elif base == 'class': # Is it a method or an attribute? return 'any' elif base == 'module': # Is it a function or a global member? return 'func' objects_inv = [] with requests.get(urljoin(url, 'api-objects.txt')) as epy: for name, uri in (line.strip().split() for line in epy.text.split('\n') if line.strip()): role = guess_epydoc_role(name, uri) objects_inv.append('{name} py:{role} 1 {uri} -'.format(name = name, role = role, uri = uri)) if objects_inv: with tempfile.NamedTemporaryFile(mode = 'wb', delete = False) as translated: translated.write('\n'.join([ "# Sphinx inventory version 2", "# Project: {} {}".format('python-vlc', '2.2'), "# Version: {}".format('2.2.0-git-14816-gda488a7751100'), "# The remainder of this file is compressed using zlib.", "" ]).encode('ascii')) translated.write(codecs.encode('\n'.join(objects_inv + [""]).encode('ascii'), 'zlib')) filename = translated.name return (url, filename) # Link to outside documentations intersphinx_mapping = { 'Gtk': ('https://lazka.github.io/pgi-docs/Gtk-3.0', None), 'Gdk': ('https://lazka.github.io/pgi-docs/Gdk-3.0', None), 'GdkPixbuf': ('https://lazka.github.io/pgi-docs/GdkPixbuf-2.0', None), 'GObject': ('https://lazka.github.io/pgi-docs/GObject-2.0', None), 'Poppler': ('https://lazka.github.io/pgi-docs/Poppler-0.18', None), 'Pango': ('https://lazka.github.io/pgi-docs/Pango-1.0', None), 'GLib': ('https://lazka.github.io/pgi-docs/GLib-2.0', None), 'GdkX11': ('https://lazka.github.io/pgi-docs/GdkX11-3.0', None), 'python': ('https://docs.python.org/{}.{}'.format(*sys.version_info[:2]), None), 'cairo': ('https://www.cairographics.org/documentation/pycairo/3', None), 'vlc': load_epydoc_as_intersphinx_v2('https://www.olivieraubert.net/vlc/python-ctypes/doc/') } # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Read the docs theme import sphinx_rtd_theme # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. # " v documentation" by default. # html_title = "Pympress developer documentation" # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = "Pympress v{}".format(version) # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = '../pympress/share/pixmaps/pympress.png' # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = '../pympress/share/pixmaps/pympress.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = '%d %B, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'pympressdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'pympress.tex', u'pympress documentation', u'Thomas Jost, Cimbali', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pympress', u'pympress documentation', [u'Thomas Jost, Cimbali'], 1) ] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'pympress', 'pympress Documentation', author, 'pympress', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # The basename for the epub file. It defaults to the project name. # epub_basename = project # The HTML theme for the epub output. Since the default themes are not # optimized for small screen space, using the same theme for HTML and epub # output is usually not wise. This defaults to 'epub', a theme designed to save # visual space. # # epub_theme = 'epub' # The language of the text. It defaults to the language option # or 'en' if the language is not set. # # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # # epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. # # epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # The depth of the table of contents in toc.ncx. # # epub_tocdepth = 3 # Allow duplicate toc entries. # # epub_tocdup = True # Choose between 'default' and 'includehidden'. # # epub_tocscope = 'default' # Fix unsupported image types using the Pillow. # # epub_fix_images = False # Scale large images. # # epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. # # epub_show_urls = 'inline' # If false, no index is generated. # # epub_use_index = True pympress-1.7.1/docs/index.md000066400000000000000000000003501415371354200157660ustar00rootroot00000000000000# Welcome to pympress's documentation! ## Contents - [Readme](../README.md) - [Configuration file](./options.md) - [Module documentation](./pympress.md) ## Indices and tables ```eval_rst * :ref:`genindex` * :ref:`modindex` ``` pympress-1.7.1/docs/options.md000066400000000000000000000136201415371354200163560ustar00rootroot00000000000000# Configuration file Pympress has a number of options available from its configuration file. This file is usually located in: - `~/.config/pympress` on Linux, - `%APPDATA%/pympress.ini` on Windows, - `~/Library/Preferences/pympress` on macOS, - in the top-level of the pympress install directory for portable installations. The path to the currently used configuration file can be checked in the `Help > About` information window. ## Shortcuts The shortcuts are parsed using [`Gtk.accelerator_parse()`](https://lazka.github.io/pgi-docs/#Gtk-3.0/functions.html#Gtk.accelerator_parse): > The format looks like “\a” or “\\F1” or “\z” (the last one is for key release). > > The parser is fairly liberal and allows lower or upper case, and also abbreviations such as “\” and “\”. Key names are parsed using [`Gdk.keyval_from_name()`](https://lazka.github.io/pgi-docs/#Gdk-3.0/functions.html#Gdk.keyval_from_name). For character keys the name is not the symbol, but the lowercase name, e.g. one would use “\minus” instead of “\-”. This means that any value in this [list of key constants](https://lazka.github.io/pgi-docs/#Gdk-3.0/constants.html#Gdk.KEY_0) is valid (removing the initial `Gdk.KEY_` part). You can verify that this value is parsed correctly from the `Help > Shortcuts` information window. ## Layouts The panes (current slide, next slide, notes, annotations, etc.) can be rearranged arbitrarily by setting the entries of the `layout` section in the configuration file. Here are a couple examples of layouts, with `Cu` the current slide, `No` the notes half of the slide, `Nx` the next slide: - All-horizontal layout: +----+----+----+ | Cu | No | Nx | +----+----+----+ Setting: notes = {"children": ["current", "notes", "next"], "proportions": [0.33, 0.33, 0.33], "orientation": "horizontal", "resizeable": true} - All-vertical layout: +----+ | Cu | +----+ | No | +----+ | Nx | +----+ Setting: notes = {"children": ["current", "notes", "next"], "proportions": [0.33, 0.33, 0.33], "orientation": "vertical", "resizeable": true} - Vertical layout with horizontally divided top pane: +----+----+ | Cu | No | +----+----+ | Nx | +---------+ Setting: notes = {"children": [ {"children": ["current", "notes"], "proportions": [0.5, 0.5], "orientation": "horizontal", "resizeable": true}, "next" ], "proportions": [0.5, 0.5], "orientation": "vertical", "resizeable": true} - Horizontal layout with horizontally divided right pane: +----+----+ | | Nx | + Cu +----+ | | No | +---------+ Setting: notes = {"children": [ "current", {"children": ["next", "notes"], "proportions": [0.5, 0.5], "orientation": "vertical", "resizeable": true} ], "proportions": [0.5, 0.5], "orientation": "horizontal", "resizeable": true} And so on. You can play with the items, their nesting, their order, and the orientation in which a set of widgets appears. For each entry the widgets (strings that are leaves of "children" nodes in this representation) must be: - for `notes`: "current", "notes", "next" - for `plain`: "current", "next" and "annotations" (the annotations widget is toggled with the `A` key by default) - for `highlight`: same as `plain` with "highlight" instead of "current" A few further remarks: - If you set "resizeable" to `false`, the panes won’t be resizeable dynamically with a handle in the middle - "proportions" are normalized, and saved on exit if you resize panes during the execution. If you set them to `4` and `1`, the panes will be `4 / (4 + 1) = 20%` and `1 / (4 + 1) = 100%`, so the ini will contain something like `0.2` and `0.8` after executing pympress. ## Themes on Windows Pympress uses the default Gtk theme of your system, which makes it easy to change on many OSs either globally via your Gtk preferences or [per application](https://www.linuxuprising.com/2019/10/how-to-use-different-gtk-3-theme-for.html). Here’s the way to do it on windows: 1. Install a theme There are 2 locations, either install the theme for all your gtk apps, e.g. in `C:\Users\%USERNAME%\AppData\Local\themes`, or just for pympress, so in `%INSTALLDIR%\share\themes` (for me that’s `C:\Users\%USERNAME%\AppData\Local\Programs\pympress\share\themes`) Basically pick a theme [e.g. from this list of dark themes](https://www.gnome-look.org/browse/cat/135/ord/rating/?tag=dark) and make sure to unpack it in the selected directory, it needs at least `%THEMENAME%\gtk-3.0\gtk.css` and `%THEMENAME%\index.theme`, where `THEMENAME` is the name of the theme. There are 2 pitfalls to be aware of, to properly install a theme: - themes that are not self-contained (relying on re-using css from default linux themes that you might not have), and - linux links (files under gtk-3.0/ that point to a directory above and that need to be replaced by a directory containing the contents of the target directory that has the same name as the link file). 2. Set the theme as default Create a `settings.ini` file, either under `C:\Users\%USERNAME%\AppData\Local\gtk-3.0` (global setting) or `%INSTALLDIR%\etc\gtk-3.0` (just pympress) and set the contents: ```ini [Settings] gtk-theme-name=THEMENAME ``` Here’s what it looks like with the [Obscure-Orange](https://www.gnome-look.org/p/1254680/) theme. ![VirtualBox_Win64_16_05_2021_01_23_19](https://user-images.githubusercontent.com/6126377/118380851-70d3d080-b5e5-11eb-97ac-65961f343a2d.png) In testing this found these 2 stackoverflow questions useful: - [Change GTK+3 look on Windows](https://stackoverflow.com/a/39041558/1387346) which contains a list of all interesting directories - [How to get native windows decorations on GTK3 on Windows 7+ and MSYS2](https://stackoverflow.com/a/37060369/1387346) which details the process pympress-1.7.1/docs/pympress.md000066400000000000000000000041121415371354200165410ustar00rootroot00000000000000# Pympress package This page contains the inline documentation, generated from the code using sphinx. The code is documented in the source using the [Google style](https://google.github.io/styleguide/pyguide.html) for docstrings. Sphinx has gathered a [set of examples](http://www.sphinx-doc.org/en/latest/ext/example_google.html) which serves as a better crash course than the full style reference. Retructured text (rst) can be used inside the comments and docstrings. ## Modules ```eval_rst .. automodule:: pympress.__main__ :members: :undoc-members: :show-inheritance: .. automodule:: pympress.app :members: :undoc-members: :show-inheritance: .. automodule:: pympress.ui :members: :undoc-members: :show-inheritance: .. automodule:: pympress.document :members: :undoc-members: :show-inheritance: .. automodule:: pympress.builder :members: :undoc-members: :show-inheritance: .. automodule:: pympress.surfacecache :members: :undoc-members: :show-inheritance: .. automodule:: pympress.scribble :members: :undoc-members: :show-inheritance: .. automodule:: pympress.pointer :members: :undoc-members: :show-inheritance: .. automodule:: pympress.editable_label :members: :undoc-members: :show-inheritance: .. automodule:: pympress.talk_time :members: :undoc-members: :show-inheritance: .. automodule:: pympress.config :members: :undoc-members: :show-inheritance: .. automodule:: pympress.extras :members: :undoc-members: :show-inheritance: .. automodule:: pympress.util :members: :undoc-members: :show-inheritance: .. automodule:: pympress.media_overlays.base :members: :undoc-members: :show-inheritance: .. automodule:: pympress.media_overlays.gif_backend :members: :undoc-members: :show-inheritance: .. automodule:: pympress.media_overlays.gst_backend :members: :undoc-members: :show-inheritance: .. automodule:: pympress.media_overlays.vlc_backend :members: :undoc-members: :show-inheritance: ``` pympress-1.7.1/pympress/000077500000000000000000000000001415371354200152715ustar00rootroot00000000000000pympress-1.7.1/pympress/__init__.py000066400000000000000000000025751415371354200174130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # __init__.py # # Copyright 2015 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ A simple and powerful dual-screen PDF reader designed for presentations. """ # # DON'T IMPORT ANYTHING HERE (OR YOU WILL BREAK setup.py) # __version__ = '1.7.1' __author__ = """2009, 2010 Thomas Jost 2015-2020 Cimbali 2016 Christoph Rath 2016 Epithumia """ __all__ = ['app', 'builder', 'config', 'document', 'editable_label', 'extras', 'media_overlays', 'pointer', 'scribble', 'surfacecache', 'talk_time', 'ui', 'util'] pympress-1.7.1/pympress/__main__.py000066400000000000000000000100761415371354200173670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # pympress # # Copyright 2009, 2010 Thomas Jost # Copyright 2015 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.__main__` -- The entry point of pympress ------------------------------------------------------- """ import logging import os import sys import locale import gettext from pympress import util # Setup logging, and catch all uncaught exceptions in the log file. # Load pympress.util early (OS and path-specific things) to load and setup gettext translation asap. logger = logging.getLogger(__name__) logging.basicConfig(filename=util.get_log_path(), level=logging.DEBUG) def uncaught_handler(*exc_info): """ Exception handler, to log uncuaght exceptions to our log file. """ logger.critical('Uncaught exception:\n{}'.format(logging.Formatter().formatException(exc_info))) sys.__excepthook__(*exc_info) sys.excepthook = uncaught_handler if util.IS_WINDOWS: if os.getenv('LANG') is None: lang, enc = locale.getdefaultlocale() os.environ['LANG'] = lang locale.setlocale(locale.LC_ALL, '') gettext.install('pympress', util.get_locale_dir()) try: # python <3.6 does not have this ModuleNotFoundError except NameError: ModuleNotFoundError = ImportError # Load python bindings for gobject introspections, aka pygobject, aka gi, and pycairo. # These are dependencies that are not specified in the setup.py, so we need to start here. # They are not specified because: # - installing those via pip requires compiling (always for pygobject, if no compatible wheels exist for cairo), # - compiling requires a compiling toolchain, development packages of the libraries, etc., # - all of this makes more sense to be handled by the OS package manager, # - it is hard to make pretty error messages pointing this out at `pip install` time, # as they would have to be printed when the dependency resolution happens. # See https://github.com/Cimbali/pympress/issues/100 try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GLib, Gio import cairo except ModuleNotFoundError: logger.critical('Gobject Introspections and/or pycairo module is missing', exc_info = True) print('\n' + _('ERROR: Gobject Introspections and/or pycairo module is missing, ' + 'make sure Gtk, pygobject and pycairo are installed on your system.') + '\n') print(_('Try your operating system’s package manager, or try running: pip install pygobject pycairo')) print(_('pip will then download and compile pygobject and pycairo, ' + 'for which you need the Gtk and cairo headers (or development packages).') + '\n') print(_('For instructions, refer to https://github.com/Cimbali/pympress/blob/master/README.md#dependencies')) print(_('If using a virtualenv or anaconda, you can also try allowing system site packages.')) print() exit(1) # Finally the real deal: load pympress modules, handle command line args, and start up from pympress import app def main(argv = sys.argv[:]): """ Entry point of pympress. Parse command line arguments, instantiate the UI, and start the main loop. """ app.Pympress().run(argv) if __name__ == "__main__": main() ## # Local Variables: # mode: python # indent-tabs-mode: nil # py-indent-offset: 4 # fill-column: 80 # end: pympress-1.7.1/pympress/app.py000066400000000000000000000301731415371354200164270ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # pympress # # Copyright 2009, 2010 Thomas Jost # Copyright 2015 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.app` -- The Gtk.Application managing the lifetime and CLI ------------------------------------------------------------------------ """ import logging logger = logging.getLogger(__name__) import signal import platform import gi import cairo gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GLib, Gio from pympress import util, config, document, ui, builder class Pympress(Gtk.Application): """ Class representing the single pympress Gtk application. """ #: The :class:`~pympress.ui.UI` object that is the interface of pympress gui = None #: The :class:`~pympress.config.Config` object that holds pympress conferences config = None #: `list` of actions to be passsed to the GUI that were queued before GUI was created action_startup_queue = [] #: `bool` to automatically upgrade log level (DEBUG / INFO at init, then ERROR), False if user set log level auto_log_level = True options = { # long_name: (short_name (int), flags (GLib.OptionFlags), arg (GLib.OptionArg) 'talk-time': (ord('t'), GLib.OptionFlags.NONE, GLib.OptionArg.STRING), 'notes': (ord('N'), GLib.OptionFlags.NONE, GLib.OptionArg.STRING), 'log': (0, GLib.OptionFlags.NONE, GLib.OptionArg.STRING), 'version': (ord('v'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE), 'pause': (ord('P'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE), 'reset': (ord('r'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE), 'next': (ord('n'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE), 'prev': (ord('p'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE), 'first': (ord('f'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE), 'last': (ord('l'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE), 'blank': (ord('b'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE), 'quit': (ord('q'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE), } option_descriptions = { # long_name: (description, arg_description) 'talk-time': (_('The estimated (intended) talk time in minutes') + ' ' + _('(and optionally seconds)'), 'mm[:ss]'), 'notes': (_('Set the position of notes on the pdf page') + ' ' + _('(none, left, right, top, bottom, or after).') + ' ' + _('Overrides the detection from the file.'), ''), 'log': (_('Set level of verbosity in log file:') + ' ' + _('{}, {}, {}, {}, or {}').format('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'), ''), 'version': (_('Print version and exit'), None), 'pause': (_('Toggle pause of talk timer'), None), 'reset': (_('Reset talk timer'), None), 'next': (_('Next slide'), None), 'prev': (_('Previous slide'), None), 'first': (_('First slide'), None), 'last': (_('Last slide'), None), 'blank': (_('Blank/unblank content screen'), None), 'quit': (_('Close opened pympress instance'), None), } version_string = ' '.join([ 'Pympress:', util.get_pympress_meta()['version'], '; Python:', platform.python_version(), '; OS:', platform.system(), platform.release(), platform.version(), '; Gtk {}.{}.{}'.format(Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()), '; GLib {}.{}.{}'.format(GLib.MAJOR_VERSION, GLib.MINOR_VERSION, GLib.MICRO_VERSION), '; Poppler', document.Poppler.get_version(), document.Poppler.get_backend().value_nick, '; Cairo', cairo.cairo_version_string(), ', pycairo', cairo.version, ]) def __init__(self): GLib.set_application_name('pympress') Gtk.Application.__init__(self, application_id='io.github.pympress', flags=Gio.ApplicationFlags.HANDLES_OPEN | Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID) self.register(None) if not self.get_is_remote(): builder.Builder.setup_actions({ 'log-level': dict(activate=self.set_log_level, state=logger.getEffectiveLevel(), parameter_type=int), }, action_map=self) # Connect proper exit function to interrupt signal.signal(signal.SIGINT, self.quit) for opt in self.options: self.add_main_option(opt, *self.options[opt], *self.option_descriptions.get(opt, ['', None])) def quit(self, *args): """ Quit and ignore other arguments e.g. sent by signals. """ if self.gui.unsaved_changes(): return Gtk.Application.quit(self) return False def do_startup(self): """ Common start-up tasks for primary and remote instances. NB. super(self) causes segfaults, Gtk.Application needs to be used as base. """ self.config = config.Config() # prefere X11 on posix systems because Wayland still has some shortcomings for us, # specifically libVLC and the ability to disable screensavers if util.IS_POSIX: Gdk.set_allowed_backends('x11,*') logger.info(self.version_string) Gtk.Application.do_startup(self) def do_activate(self, timestamp=GLib.get_current_time()): """ Activate: show UI windows. Build them if they do not exist, otherwise bring to front. """ if self.gui is None: if self.auto_log_level: self.activate_action('log-level', logging.INFO) self.action_startup_queue.append(('log-level', logging.ERROR)) # Build the UI and windows self.gui = ui.UI(self, self.config) while self.action_startup_queue: self.activate_action(*self.action_startup_queue.pop(0)) Gtk.Application.do_activate(self) self.gui.p_win.present_with_time(timestamp) def set_action_enabled(self, name, value): """ Parse an action name and set its enabled state to True or False. Args: name (`str`): the name of the stateful action value (`bool`): wheether the action should be enabled or disabled """ self.lookup_action(name).set_enabled(value) def set_action_state(self, name, value): """ Parse an action name and set its state wrapped in a :class:`~GLib.Variant`. Args: name (`str`): the name of the stateful action value (`str`, `int`, `bool` or `float`): the value to set. """ self.lookup_action(name).change_state(GLib.Variant(builder.Builder._glib_type_strings[type(value)], value)) def get_action_state(self, name): """ Parse an action name and return its unwrapped state from the :class:`~GLib.Variant`. Args: name (`str`): the name of the stateful action Returns: `str`, `int`, `bool` or `float`: the value contained in the action """ state = self.lookup_action(name).get_state() return builder.Builder._glib_type_getters[state.get_type_string()](state) def activate_action(self, name, parameter=None): """ Parse an action name and activate it, with parameter wrapped in a :class:`~GLib.Variant` if it is not None. Args: name (`str`): the name of the stateful action parameter: an object or None to pass as a parameter to the action, wrapped in a GLib.Variant """ if not self.get_is_remote() and self.gui is None and name not in ['log-level']: self.action_startup_queue.append((name, parameter)) return if parameter is not None: parameter = GLib.Variant(builder.Builder._glib_type_strings[type(parameter)], parameter) Gio.ActionGroup.activate_action(self, name, parameter) def do_open(self, files, n_files, hint): """ Handle opening files. In practice we only open once, the last one. Args: files (`list` of :class:`~Gio.File`): representing an array of files to open n_files (`int`): the number of files passed. hint (`str`): a hint, such as view, edit, etc. Should always be the empty string. """ if not n_files: return self.do_activate(timestamp=GLib.get_current_time()) self.gui.swap_document(files[-1].get_uri()) def do_shutdown(self): """ Perform various cleanups and save preferences. """ if self.gui is not None: self.gui.cleanup() self.config.save_config() Gtk.Application.do_shutdown(self) def set_log_level(self, action, param): """ Action that sets the logging level (on the root logger of the active instance) Args: action (:class:`~Gio.Action`): The action activatd param (:class:~`GLib.Variant`): The desired level as an int wrapped in a GLib.Variant """ logging.getLogger(None).setLevel(param.get_int64()) action.change_state(param) def do_handle_local_options(self, opts_variant_dict): """ Parse command line options, returned as a VariantDict Returns: `tuple`: estimated talk time, log level, notes positions. """ # convert GVariantDict -> GVariant -> dict opts = opts_variant_dict.end().unpack() simple_actions = { 'pause': 'pause-timer', 'reset': 'reset-timer', 'next': 'next-page', 'prev': 'prev-page', 'blank': 'blank-screen', 'quit': 'quit', 'first': 'first-page', 'last': 'last-page', } for opt, arg in opts.items(): if opt == "version": print(self.version_string) return 0 elif opt == "log": numeric_level = getattr(logging, arg.upper(), None) if isinstance(numeric_level, int): self.auto_log_level = False self.activate_action('log-level', numeric_level) else: print(_("Invalid log level \"{}\", try one of {}").format( arg, "DEBUG, INFO, WARNING, ERROR, CRITICAL" )) elif opt == "notes": arg = arg.lower()[:1] if arg == 'n': self.activate_action('notes-pos', 'none') if arg == 'l': self.activate_action('notes-pos', 'left') if arg == 'r': self.activate_action('notes-pos', 'right') if arg == 't': self.activate_action('notes-pos', 'top') if arg == 'b': self.activate_action('notes-pos', 'bottom') if arg == 'a': self.activate_action('notes-pos', 'after') elif opt == "talk-time": t = ["0" + n.strip() for n in arg.split(':')] try: m = int(t[0]) s = int(t[1]) except ValueError: print(_("Invalid time (mm or mm:ss expected), got \"{}\"").format(arg)) return 2 except IndexError: s = 0 self.activate_action('set-talk-time', m * 60 + s) elif opt in simple_actions: self.activate_action(simple_actions[opt]) return -1 pympress-1.7.1/pympress/builder.py000066400000000000000000000360111415371354200172720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # builder.py # # Copyright 2017 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.ui_builder` -- abstract GUI management ----------------------------------------------------- This module contains the tools to load the graphical user interface of pympress, building the widgets/objects from XML (glade) files, applying translation "manually" to avoid dealing with all the mess of C/GNU gettext's bad portability. """ import logging logger = logging.getLogger(__name__) import copy from collections import deque import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject, GLib, Gio from pympress import util class Builder(Gtk.Builder): """ GUI builder, inherits from :class:`~Gtk.Builder` to read XML descriptions of GUIs and load them. """ #: `set` of :class:`~Gtk.Widget`s that have been built by the builder, and translated __built_widgets = set() #: `dict` mapping :class:`~Gtk.Paned` names to a `tuple` of (handler id of the size-allocate signal, remaining #: number of times we allow this signal to run), and we run the signal 2 * (depth + 1) for each pane. #: This is because size allocation is done bottom-up but each pane sets a top-down constraint. pending_pane_resizes = {} _glib_type_strings = { float: 'd', bool: 'b', int: 'x', str: 's', } _glib_type_getters = { 'd': GLib.Variant.get_double, 'b': GLib.Variant.get_boolean, 'x': GLib.Variant.get_int64, 's': GLib.Variant.get_string, } def __init__(self): super(Builder, self).__init__() @staticmethod def __translate_widget_strings(a_widget): """ Calls gettext on all strings we can find in a_widgets. Args: a_widget (:class:`~GObject.Object`): an object built by the builder, usually a widget """ for str_prop in (prop.name for prop in a_widget.props if prop.value_type == GObject.TYPE_STRING): try: str_val = getattr(a_widget.props, str_prop) if str_val: setattr(a_widget.props, str_prop, _(str_val)) except TypeError: # Thrown when a string property is not readable pass @staticmethod def __recursive_translate_widgets(a_widget): """ Calls gettext on all strings we can find in widgets, and recursively on its children. Args: a_widget (:class:`~GObject.Object`): an object built by the builder, usually a widget """ Builder.__translate_widget_strings(a_widget) if issubclass(type(a_widget), Gtk.Container): # NB: Parent-loop in widgets would cause infinite loop here, but that's absurd (right?) # NB2: maybe forall instead of foreach if we miss some strings? a_widget.foreach(Builder.__recursive_translate_widgets) if issubclass(type(a_widget), Gtk.MenuItem) and a_widget.get_submenu() is not None: Builder.__recursive_translate_widgets(a_widget.get_submenu()) def get_callback_handler(self, handler_name): """ Returns the handler from its name, searching in target. Parse handler names and split on '.' to use recursion. Args: target (`object`): An object that has a method called `handler_name` handler_name (`str`): The name of the function to be connected to a signal Returns: `function`: A function bound to an object """ target = self for attr in handler_name.split('.'): try: target = getattr(target, attr) except AttributeError: logger.error('Can not reach target of signal {}.{}()'.format(self, handler_name), exc_info=True) return None return target def signal_connector(self, builder, object, signal_name, handler_name, connect_object, flags, *user_data): """ Callback for signal connection. Implements the `~Gtk.BuilderConnectFunc` function interface. Args: builder (:class:`~pympress.builder.Builder`): The builder, unused object (:class:`~GObject.Object`): The object (usually a wiget) that has a signal to be connected signal_name (`str`): The name of the signal handler_name (`str`): The name of the function to be connected to the signal connect_object (:class:`~GObject.Object`): unused flags (:class:`~GObject.ConnectFlags`): unused user_data (`tuple`): supplementary positional arguments to be passed to the handler """ try: handler = self.get_callback_handler(handler_name) object.connect(signal_name, handler, *user_data) except Exception: logger.critical('Impossible to connect signal {} from object {} to handler {}' .format(signal_name, object, handler_name), exc_info = True) def connect_signals(self, base_target): """ Signal connector connecting to properties of `base_target`, or properties of its properties, etc. Args: base_target (:class:`~pympress.builder.Builder`): The target object, that has functions to be connected to signals loaded in this builder. """ Builder.connect_signals_full(base_target, self.signal_connector) def load_ui(self, resource_name, **kwargs): """ Loads the UI defined in the file named resource_name using the builder. Args: resource_name (`str`): the basename of the glade file (without extension), identifying the resource to load. """ self.add_from_file(util.get_ui_resource_file(resource_name, **kwargs)) # Get all newly built objects new_objects = set(self.get_objects()) - self.__built_widgets self.__built_widgets.update(new_objects) for obj in new_objects: # pass new objects to manual translation self.__translate_widget_strings(obj) # Instrospectively load objects. If we have a self.attr == None and this attr is the name of a built object, # link it together. if issubclass(type(obj), Gtk.Buildable): obj_id = Gtk.Buildable.get_name(obj) # set Gtk.Widget.name property to the value of the Gtk.Buildable id if issubclass(type(obj), Gtk.Widget): Gtk.Widget.set_name(obj, obj_id) if hasattr(self, obj_id) and getattr(self, obj_id) is None: setattr(self, obj_id, obj) def list_attributes(self, target): """ List the None-valued attributes of target. Args: target (`dict`): An object with None-valued attributes """ for attr in dir(target): try: if attr[:2] + attr[-2:] != '____' and getattr(target, attr) is None: yield attr except RuntimeError: pass def load_widgets(self, target): """ Fill in target with the missing elements introspectively. This means that all attributes of `target` that are None now must exist under the same name in the builder. Args: target (`dict`): An object with None-valued properties whose names correspond to ids of built widgets. """ for attr in self.list_attributes(target): setattr(target, attr, self.get_object(attr)) def replace_layout(self, layout, top_widget, leaf_widgets, pane_resize_handler=None): """ Remix the layout below top_widget with the layout configuration given in 'layout' (assumed to be valid!). Args: layout (`dict`): the json-parsed config string, thus a hierarchy of lists/dicts, with strings as leaves top_widget (:class:`~Gtk.Container`): The top-level widget under which we build the hierachyy leaf_widgets (`dict`): the map of valid leaf identifiers (strings) to the corresponding :class:`~Gtk.Widget` pane_resize_handler (function): callback function to be called when the panes are resized Returns: `dict`: The mapping of the used :class:`~Gtk.Paned` widgets to their relative handle position (in 0..1). """ # take apart the previous/default layout containers = [] widgets = top_widget.get_children() i = 0 while i < len(widgets): w = widgets[i] if w in self.placeable_widgets.values(): pass elif issubclass(type(w), Gtk.Box) or issubclass(type(w), Gtk.Paned): widgets.extend(w.get_children()) containers.append(w) w.get_parent().remove(w) i += 1 # cleanup widgets del widgets[:] while containers: containers.pop().destroy() self.pending_pane_resizes.clear() # iterate over new layout to build it, using a BFS widgets_to_add = deque([(top_widget, copy.deepcopy(layout))]) pane_resize = set() pane_handle_pos = {} while widgets_to_add: parent, w_desc = widgets_to_add.popleft() if type(w_desc) is str: w = leaf_widgets[w_desc] else: # get new container widget if 'resizeable' in w_desc and w_desc['resizeable']: orientation = getattr(Gtk.Orientation, w_desc['orientation'].upper()) w = Gtk.Paned.new(orientation) w.set_wide_handle(True) # Add on resize events if pane_resize_handler: w.connect('notify::position', pane_resize_handler) w.connect('button-release-event', pane_resize_handler) # left pane is first child widgets_to_add.append((w, w_desc['children'].pop())) if 'proportions' in w_desc: right_pane = w_desc['proportions'].pop() left_pane = w_desc['proportions'].pop() w_desc['proportions'].append(left_pane + right_pane) pane_handle_pos[w] = left_pane / (left_pane + right_pane) pane_resize.add(w) else: pane_handle_pos[w] = 0.5 hid = w.connect('size-allocate', self.resize_paned, pane_handle_pos[w]) w.set_name('GtkPaned{}'.format(len(self.pending_pane_resizes))) self.pending_pane_resizes[w.get_name()] = (hid, 2 * len(self.pending_pane_resizes) + 1) # if more than 2 children are to be added, add the 2+ from the right side in a new child Gtk.Paned widgets_to_add.append((w, w_desc['children'][0] if len(w_desc['children']) == 1 else w_desc)) else: w = Gtk.Box.new(getattr(Gtk.Orientation, w_desc['orientation'].upper()), 5) w.set_homogeneous(True) w.set_spacing(10) widgets_to_add.extend((w, c) for c in w_desc['children']) if issubclass(type(parent), Gtk.Box): parent.pack_start(w, True, True, 0) else: # it's a Gtk.Paned if parent.get_child2() is None: parent.pack2(w, True, True) if parent.get_orientation() == Gtk.Orientation.HORIZONTAL: w.set_margin_start(8) else: w.set_margin_top(8) else: parent.pack1(w, True, True) if parent.get_orientation() == Gtk.Orientation.HORIZONTAL: w.set_margin_end(8) else: w.set_margin_bottom(8) return pane_handle_pos def resize_paned(self, paned, rect, relpos): """ Resize `paned` to have its handle at `relpos`, then disconnect this signal handler. Called from the :func:`Gtk.Widget.signals.size_allocate` signal. Args: paned (:class:`~Gtk.Paned`): Panel whose size has just been allocated, and whose handle needs initial placement. rect (:class:`~Gdk.Rectangle`): The rectangle specifying the size that has just been allocated to `~paned` relpos (`float`): A number between `0.` and `1.` that specifies the handle position Returns: `True` """ size = rect.width if paned.get_orientation() == Gtk.Orientation.HORIZONTAL else rect.height handle_pos = int(round(relpos * size)) GLib.idle_add(paned.set_position, handle_pos) name = paned.get_name() handler_id, sizes = self.pending_pane_resizes[name] self.pending_pane_resizes[name] = (handler_id, sizes - 1) if sizes <= 0: paned.disconnect(handler_id) return True @staticmethod def setup_actions(actions, action_map=None): """ Sets up actions with a given prefix, using the Application as the ActionMap. Args: actions (`dict`): Maps the action names to dictionaries containing their parameters. action_map (:class:`~Gio.ActionMap`): The object implementing the action map interface to register actions """ if action_map is None: action_map = Gio.Application.get_default() for action_name, details in actions.items(): state = details.get('state') param = details.get('parameter_type') enabled = details.get('enabled') if param is not None: param = GLib.VariantType.new(Builder._glib_type_strings[param]) if state is not None: state = GLib.Variant(Builder._glib_type_strings[type(state)], state) action = Gio.SimpleAction.new_stateful(action_name, param, state) else: action = Gio.SimpleAction.new(action_name, param) for event in ['activate', 'change-state']: handler = details.get(event.replace('-', '_')) if handler is not None: action.connect(event, handler) action_map.add_action(action) if enabled is not None: action.set_enabled(enabled) return action_map pympress-1.7.1/pympress/config.py000066400000000000000000000527701415371354200171230ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # config.py # # Copyright 2017 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.config` -- Configuration --------------------------------------- """ import logging logger = logging.getLogger(__name__) import pathlib import json import configparser from collections import deque import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GLib, Gio from pympress import util class Config(configparser.ConfigParser, object): # python 2 fix """ Manage configuration :Get the configuration from its file and store its back. """ #: `dict`-tree of presenter layouts for various modes layout = {} #: `dict` of strings that are the valid representations of widgets from the presenter window #: that can be dynamically rearranged, mapping to their names placeable_widgets = {"notes": "p_frame_notes", "current": "p_frame_cur", "next": "grid_next", "annotations": "p_frame_annot", "highlight": "scribble_overlay"} #: `dict` mapping layout ids to tuples of their expected and optional widgets widget_reqs = { 'notes': (set(placeable_widgets.keys()) - {"annotations", "highlight"}, {"annotations"}), 'plain': (set(placeable_widgets.keys()) - {"notes", "highlight"},), 'note_pages': (set(placeable_widgets.keys()) - {"current", "highlight"},), 'highlight': ({"highlight"}, set(placeable_widgets.keys()) - {"highlight"}) } #: `dict` mapping accelerator keys to actions shortcuts = {} @staticmethod def path_to_config(search_legacy_locations=False): """ Return the path to the currently used configuration file. Args: search_legacy_locations (`bool`): whether to look in previously used locations Returns: :class:`~pathlib.Path`: The path to the config file to use """ portable_config = util.get_portable_config() if portable_config.exists(): return portable_config user_config = util.get_user_config() # migrate old configuration files from previously-used erroneous locations if search_legacy_locations and (util.IS_POSIX or util.IS_MAC_OS) and not user_config.exists(): for legacy_location in ['~/.pympress', '~/.config/pympress']: legacy_location = pathlib.Path(legacy_location).expanduser() if legacy_location.exists(): legacy_location.rename(user_config) return user_config @staticmethod def toggle_portable_config(gaction, param=None): """ Create or remove a configuration file for portable installs. The portable install file will be used by default, and deleting it causes the config to fall back to the user profile location. No need to populate the new config file, this will be done on pympress exit. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ portable_config = util.get_portable_config() if Config.using_portable_config(): portable_config.unlink() else: with open(portable_config, 'w'): pass gaction.set_state(GLib.Variant.new_boolean(Config.using_portable_config())) @staticmethod def using_portable_config(): """ Checks which configuration file location is in use. Returns: `bool`: `True` iff we are using the portable (i.e. in install dir) location """ return util.get_portable_config().exists() def __init__(config): # Remove : from delimiters so we can use it in preferences super(Config, config).__init__(delimiters=('=',)) # populate values first from the default config file, then from the proper one config.read(util.get_default_config()) config.load_window_layouts() all_commands = dict(config.items('shortcuts')).keys() config.read(config.path_to_config(True)) config.upgrade() config.load_window_layouts() for command in all_commands: # NB only parsing commands from defaults parse_ok, action_name, target_value = Gio.Action.parse_detailed_name('app.' + command) if not parse_ok or not Gio.action_name_is_valid(action_name): logger.error('Failed parsing command ' + command) continue parsed_accels = {keys: Gtk.accelerator_parse(keys) for keys in config.get('shortcuts', command).split()} failed = [keys for keys, parsed in parsed_accels.items() if parsed == (0, 0)] if failed: logger.warning('Failed parsing shortcut(s) for "{}": "{}"'.format(command, '", "'.join(failed))) keep_accels = [keys for keys, parsed in parsed_accels.items() if parsed != (0, 0)] if keep_accels: config.shortcuts[command] = keep_accels def register_actions(self, builder): """ Register actions that impact the config file only. Args: builder (:class:`pympress.builder.Builder`): a builder to setup the actions """ p_full = self.getboolean('presenter', 'start_fullscreen') c_full = self.getboolean('content', 'start_fullscreen') blank = self.getboolean('content', 'start_blanked') portable = self.using_portable_config() builder.setup_actions({ 'start-content-fullscreen': dict(activate=self.toggle_start, state=c_full), 'start-presenter-fullscreen': dict(activate=self.toggle_start, state=p_full), 'start-blanked': dict(activate=self.toggle_start, state=blank), 'portable-config': dict(activate=self.toggle_portable_config, state=portable), }) def upgrade(self): """ Update obsolete config options when pympress updates. """ if self.get('presenter', 'pointer') == 'pointer_none': self.set('presenter', 'pointer', 'red') self.set('presenter', 'pointer_mode', 'disabled') if self.has_section('scribble') and self.has_option('scribble', 'color'): self.set('scribble', 'color_9', self.get('scribble', 'color')) self.remove_option('scribble', 'color') self.set('scribble', 'active_pen', '9') if self.has_section('scribble') and self.has_option('scribble', 'width'): self.set('scribble', 'width_9', self.get('scribble', 'width')) self.remove_option('scribble', 'width') self.set('scribble', 'active_pen', '9') if self.has_option('presenter', 'monitor'): self.remove_option('presenter', 'monitor') if self.has_option('content', 'monitor'): self.remove_option('content', 'monitor') if self.has_section('scribble'): for key, val in self.items('scribble'): self.set('highlight', key, val) self.remove_section('scribble') if self.has_section('gst'): for key, val in self.items('gst'): self.set('gstreamer', key, 'on' if key == 'enabled' else val) self.remove_section('gst') # When we went from gtk signal handlers to actions, some renaming had to be done for old, new in { 'next': 'next-page', 'prev': 'prev-page', 'next_label': 'next-label', 'prev_label': 'prev-label', 'hist_back': 'hist-back', 'hist_forward': 'hist-forward', 'first': 'first-page', 'last': 'last-page', 'goto_page': 'goto-page', 'jumpto_label': 'jumpto-label', 'fullscreen_content': 'content-fullscreen', 'fullscreen_presenter': 'presenter-fullscreen', 'pause_timer': 'pause-timer', 'reset_timer': 'reset-timer', 'talk_time': 'edit-talk-time', 'blank_screen': 'blank-screen', 'notes_mode': 'notes-mode', 'swap_screens': 'swap-screens', 'open_file': 'pick-file', 'close_file': 'close-file', 'validate': 'validate-input', 'cancel': 'cancel-input', 'undo_scribble': 'highlight-undo', 'redo_scribble': 'highlight-redo', 'scribble_preset_1': 'highlight-use-pen::1', 'scribble_preset_2': 'highlight-use-pen::2', 'scribble_preset_3': 'highlight-use-pen::3', 'scribble_preset_4': 'highlight-use-pen::4', 'scribble_preset_5': 'highlight-use-pen::5', 'scribble_preset_6': 'highlight-use-pen::6', 'scribble_preset_7': 'highlight-use-pen::7', 'scribble_preset_8': 'highlight-use-pen::8', 'scribble_preset_9': 'highlight-use-pen::9', 'scribble_preset_0': 'highlight-use-pen::eraser', 'toggle_pointermode': 'pointer-mode::toggle', }.items(): shortcut = self.get('shortcuts', old, fallback=None) if shortcut is not None: if old in {'hist_back', 'hist_forward'} and 'backspace' in shortcut.lower(): # In the Gio.Action operations, accelerators have precedence over widgets, which # means it’s very annoying to edit a shortcut if backspace is mapped to anything. logger.warning('Changing shortcut for "{}" to new default instead of keeping backspace'.format(new)) else: self.set('shortcuts', new, shortcut) self.remove_option('shortcuts', old) def getlist(self, *args): """ Parse a config value and return the list by splitting the value on commas. i.e. bar = foo,qux returns the list ['foo', 'qux'] Returns: `list`: a config value split into a list. """ return [t.strip() for t in self.get(*args).split(',') if t.strip()] def getint(self, *args, **kwargs): """ Wrapper for configparser’s getint to handle parsing errors when a fallback is given. See :meth:`~configparser.Configparser.getint()` """ try: return super(Config, self).getint(*args, **kwargs) except ValueError: if 'fallback' not in kwargs: raise logger.warning(_('Error parsing option from config file {}.{} "{}" to int'.format(*args, self.get(*args))), exc_info=True) return kwargs['fallback'] def getfloat(self, *args, **kwargs): """ Wrapper for configparser’s to handle parsing errors when a fallback is given. See :meth:`~configparser.Configparser.getfloat()` """ try: return super(Config, self).getfloat(*args, **kwargs) except ValueError: if 'fallback' not in kwargs: raise logger.warning(_('Error parsing option from config file {}.{} "{}" to float') .format(*args, self.get(*args)), exc_info=True) return kwargs['fallback'] def getboolean(self, *args, **kwargs): """ Wrapper for configparser’s getboolean to handle parsing errors when a fallback is given. :meth:`~configparser.Configparser.getboolean()` """ try: return super(Config, self).getboolean(*args, **kwargs) except ValueError: if 'fallback' not in kwargs: raise logger.warning(_('Error parsing option from config file {}.{} "{}" to bool'.format(*args, self.get(*args))), exc_info=True) return kwargs['fallback'] def save_config(self): """ Save the configuration to its file. """ # serialize the layouts for layout_name in self.layout: self.set('layout', layout_name, json.dumps(self.layout[layout_name], indent=4)) with open(self.path_to_config(), 'w') as configfile: self.write(configfile) def toggle_start(self, gaction, param=None): """ Generic function to toggle some boolean startup configuration. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ # action is named start(-presenter|-content)?-property start, *win, prop = gaction.get_name().split('-') window = win[0] if win else 'content' start_conf = 'start_' + prop new_state = not gaction.get_state().get_boolean() gaction.set_state(GLib.Variant.new_boolean(new_state)) self.set(window, start_conf, 'on' if new_state else 'off') def validate_layout(self, layout, expected_widgets, optional_widgets = set()): """ Validate layout: check whether the layout of widgets built from the config string is valid. Args: layout (`dict`): the json-parsed config string expected_widgets (`set`): strings with the names of widgets that have to be used in this layout optional_widgets (`set`): strings with the names of widgets that may or may not be used in this layout Layout must have all self.placeable_widgets (leaves of the tree, as `str`) and only allowed properties on the nodes of the tree (as `dict`). Constraints on the only allowed properties of the nodes are: - resizeable: `bool` (optional, defaults to no), - orientation: `str`, either "vertical" or "horizontal" (mandatory) - children: `list` of size >= 2, containing `str`s or `dict`s (mandatory) - proportions: `list` of `float` with sum = 1, length == len(children), representing the relative sizes of all the resizeable items (if and only if resizeable). """ next_visits = deque([layout]) widget_seen = set() while next_visits: w_desc = next_visits.popleft() if type(w_desc) is str: if w_desc not in expected_widgets and w_desc not in optional_widgets: raise ValueError('Unrecognized widget "{}", pick one of: {}' .format(w_desc, ', '.join(expected_widgets))) elif w_desc in widget_seen: raise ValueError('Duplicate widget "{}", all expected_widgets can only appear once'.format(w_desc)) widget_seen.add(w_desc) elif type(w_desc) is dict: if 'orientation' not in w_desc or w_desc['orientation'] not in ['horizontal', 'vertical']: raise ValueError('"orientation" is mandatory and must be "horizontal" or "vertical" at node {}' .format(w_desc)) elif 'children' not in w_desc or type(w_desc['children']) is not list or len(w_desc['children']) < 2: raise ValueError('"children" is mandatory and must be a list of 2+ items at node {}'.format(w_desc)) elif 'resizeable' in w_desc and type(w_desc['resizeable']) is not bool: raise ValueError('"resizeable" must be boolean at node {}'.format(w_desc)) elif 'proportions' in w_desc: if 'resizeable' not in w_desc or not w_desc['resizeable']: raise ValueError('"proportions" is only valid for resizeable widgets at node {}'.format(w_desc)) elif type(w_desc['proportions']) is not list or \ any(type(n) is not float for n in w_desc['proportions']) or \ len(w_desc['proportions']) != len(w_desc['children']) or \ abs(sum(w_desc['proportions']) - 1) > 1e-10: raise ValueError('"proportions" must be a list of floats (one per separator), ' - 'between 0 and 1, at node {}'.format(w_desc)) next_visits.extend(w_desc['children']) else: raise ValueError('Unexpected type {}, nodes must be dicts or strings, at node {}' .format(type(w_desc), w_desc)) widget_missing = expected_widgets - widget_seen if widget_missing: raise ValueError('Following placeable_widgets were not specified: {}'.format(', '.join(widget_missing))) def update_layout_tree(self, layout_name, layout): """ Update the layout named `~layout_name`. Throws ValueError on invalid layouts. Args: layout_name (`str`): the name of the layout to update layout (`dict`): the hierarchy of dictionaries, lists, and strings representing the layout """ self.validate_layout(layout, *self.widget_reqs[layout_name]) self.layout[layout_name] = layout def load_window_layouts(self): """ Parse and validate layouts loaded from config, with fallbacks if needed. """ for layout_name in self.widget_reqs: # Log error and keep default layout try: self.update_layout_tree(layout_name, json.loads(self.get('layout', layout_name))) except ValueError: logger.exception('Invalid layout for {}'.format(layout_name)) def widget_layout_to_tree(self, widget, pane_handle_pos): """ Build a tree representing a widget hierarchy, leaves are strings and nodes are `dict`. Recursive function. See validate_layout() for more info on the tree structure. Args: widget (:class:`~Gtk.Widget`): the widget where to start pane_handle_pos (`dict`): Map of :class:`~Gtk.Paned` to the relative handle position (float in 0..1) Returns: `dict`: A tree of dicts reprensenting the widget hierarchy """ orientation_names = {Gtk.Orientation.HORIZONTAL: 'horizontal', Gtk.Orientation.VERTICAL: 'vertical'} name = widget.get_name() matching_widget_names = [k for k, v in self.placeable_widgets.items() if v == name] if matching_widget_names: return matching_widget_names[0] elif issubclass(type(widget), Gtk.Box): return {'resizeable': False, 'orientation': orientation_names[widget.get_orientation()], 'children': [self.widget_layout_to_tree(c, pane_handle_pos) for c in widget.get_children()]} elif issubclass(type(widget), Gtk.Paned): proportions = [1] reverse_children = [] orientation = widget.get_orientation() if orientation == Gtk.Orientation.HORIZONTAL: get_size = Gtk.Widget.get_allocated_width else: get_size = Gtk.Widget.get_allocated_height while issubclass(type(widget), Gtk.Paned) and orientation == widget.get_orientation(): left_pane = widget.get_child1() right_pane = widget.get_child2() visible = left_pane.get_visible() and right_pane.get_visible() position = widget.get_position() widget_size = get_size(widget) if not visible or widget_size <= 1: # reuse number that was in config initially, otherwise gets overwritten with 0 ratio = pane_handle_pos[widget] else: ratio = float(position) / widget_size pane_handle_pos[widget] = ratio proportions = [ratio] + [(1 - ratio) * p for p in proportions] reverse_children.append(right_pane) widget = left_pane reverse_children.append(left_pane) return {'resizeable': True, 'proportions': proportions, 'orientation': orientation_names[orientation], 'children': [self.widget_layout_to_tree(c, pane_handle_pos) for c in reversed(reverse_children)]} raise ValueError('Error serializing layout: widget of type {} '.format(type(widget)) + 'is not an expected container or named widget: "{}"'.format(name)) def get_layout(self, layout_name): """ Getter for the `~layout_name` layout. """ return self.layout[layout_name] def update_layout_from_widgets(self, layout_name, widget, pane_handle_pos): """ Setter for the notes layout. Args: layout_name (`str`): the name of the layout to update widget (:class:`~Gtk.Widget`): the widget that will contain the layout. pane_handle_pos (`dict`): Map of :class:`~Gtk.Paned` to the relative handle position (float in 0..1) """ self.update_layout_tree(layout_name, self.widget_layout_to_tree(widget, pane_handle_pos)) pympress-1.7.1/pympress/dialog.py000066400000000000000000000605771415371354200171210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # extras.py # # Copyright 2021 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.extras` -- Manages the display of fancy extras such as annotations, videos and cursors ----------------------------------------------------------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import sys import copy import itertools import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GLib from pympress import builder class TimingReport(builder.Builder): """ Widget tracking and displaying hierachically how much time was spent in each page/section of the presentation. """ #: `list` of time at which each page was reached page_time = [] #: `int` the time at which the clock was reset end_time = -1 #: The :class:`~Gtk.TreeView` containing the timing data to display in the dialog timing_treeview = None #: A :class:`~Gtk.Dialog` to contain the timing to show time_report_dialog = None #: `bool` marking whether next page transition should reset the history of page timings clear_on_next_transition = False #: A `dict` containing the structure of the current document doc_structure = {} #: A `list` with the page label of each page of the current document page_labels = [] #: `bool` tracking whether a document is opened document_open = False def __init__(self, parent): super(TimingReport, self).__init__() self.load_ui('time_report_dialog') self.time_report_dialog.set_transient_for(parent.p_win) self.time_report_dialog.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) self.connect_signals(self) parent.setup_actions({ 'timing-report': dict(activate=self.show_report), }) def transition(self, page, time): """ Record a transition time between slides. Args: page (`int`): the page number of the current slide time (`int`): the number of seconds elapsed since the beginning of the presentation """ if not self.document_open: return if self.clear_on_next_transition: self.clear_on_next_transition = False del self.page_time[:] self.page_time.append((page, time)) def reset(self, reset_time): """ A timer reset. Clear the history as soon as we start changing pages again. """ self.end_time = reset_time self.clear_on_next_transition = True @staticmethod def format_time(secs): """ Formats a number of seconds as `minutes:seconds`. Returns: `str`: The formatted time, with 2+ digits for minutes and 2 digits for seconds. """ return '{:02}:{:02}'.format(*divmod(secs, 60)) def set_document_metadata(self, doc_structure, page_labels): """ Show the popup with the timing infortmation. Args: doc_structure (`dict`): the structure of the document page_labels (`list`): the page labels for each of the pages """ self.document_open = len(page_labels) != 0 # Do not update if we only close the document. # That way, the report is still accessible when the document is closed. if not self.document_open: return self.doc_structure = doc_structure self.page_labels = page_labels # Clear the report when there is a new document opened. del self.page_time[:] def show_report(self, gaction, param=None): """ Show the popup with the timing infortmation. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ times = [time for page, time in self.page_time] durations = (e - s for s, e in zip(times, times[1:] + [self.end_time])) min_time = min(time for page, time in self.page_time) if self.page_time else 0 infos = {'time': min_time, 'duration': 0, 'children': [], 'page': 0} infos['title'] = 'Full presentation' for (page, start_time), duration in zip(self.page_time, durations): if not duration: continue infos['duration'] += duration # lookup the position of the page in the document structure (section etc) lookup = self.doc_structure cur_info_pos = infos while lookup: try: pos = max(p for p in lookup if p <= page) except ValueError: break item = lookup[pos] lookup = item.get('children', None) if cur_info_pos['children'] and cur_info_pos['children'][-1]['page'] == pos: cur_info_pos['children'][-1]['duration'] += duration else: cur_info_pos['children'].append({'page': pos, 'title': item['title'], 'children': [], 'duration': duration, 'time': start_time}) cur_info_pos = cur_info_pos['children'][-1] # add the actual page as a leaf node label = self.page_labels[page] if 0 <= page < len(self.page_labels) else 'None' cur_info_pos['children'].append({'page': page, 'title': _('slide #') + label, 'duration': duration, 'time': start_time}) treemodel = self.timing_treeview.get_model() if treemodel: treemodel.clear() treemodel = Gtk.TreeStore(str, str, str, str) dfs_info = [(None, infos)] while dfs_info: first_it, first = dfs_info.pop() page = first['page'] label = self.page_labels[page] if 0 <= page < len(self.page_labels) else 'None' last_col = '{} ({}/{})'.format(label, page, len(self.page_labels)) row = [first['title'], self.format_time(first['time']), self.format_time(first['duration']), last_col] it = treemodel.append(first_it, row) if 'children' in first: dfs_info.extend((it, child) for child in reversed(first['children'])) self.timing_treeview.set_model(treemodel) self.timing_treeview.expand_row(Gtk.TreePath.new_first(), False) self.time_report_dialog.run() self.time_report_dialog.hide() class LayoutEditor(builder.Builder): """ Widget tracking and displaying hierachically how much time was spent in each page/section of the presentation. """ #: The :class:`~Gtk.TreeView` displaying the hierarchical layouts layout_treeview = None #: The :class:`~Gtk.TreeModel` containing the model of the layouts to view in the treeview layout_treemodel = None #: The :class:`~Gtk.ListModel` containing the possible orientations orientations_model = None #: A :class:`~Gtk.Dialog` to contain the layout edition dialog layout_dialog = None #: A :class:`~Gtk.Label` to contain the description of the layout layout_description = None #: A :class:`~Gtk.ComboBoxText` to select the layout to edit layout_selector = None #: :class:`~pympress.config.Config` to remember preferences config = None #: :class:`~Gio.Action` containing the number of next frames next_frames_action = None #: :class:`~Gio.Action` containing the orientation hltools_orientation_action = None #: `str` containing the layout currently edited current_layout = 'plain' #: callback, to be connected to :func:`~pympress.ui.UI.load_layout` ui_load_layout = lambda *args: None layout_descriptions = { 'notes': _('Layout for beamer notes on second screen (no current slide preview in notes)'), 'plain': _('Plain layout, without note slides'), 'note_pages': _('Layout for libreoffice notes on separate pages (with current slide preview in notes)'), 'highlight': _('Layout to draw on the current slide'), } _model_columns = ['widget', 'has_resizeable', 'resizeable', 'has_orientation', 'orientation', 'next_slide_count', 'widget_name'] def __init__(self, parent, config): super(LayoutEditor, self).__init__() self.load_ui('layout_dialog') self.layout_dialog.set_transient_for(parent.p_win) self.layout_dialog.add_button(Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY) self.layout_dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) self.layout_selector.get_child().set_editable(False) self.config = config self.ui_load_layout = parent.get_callback_handler('load_layout') self.connect_signals(self) parent.setup_actions({ 'edit-layout': dict(activate=self.show_editor), }) def load_layout(self): """ Load the given layout in the treemodel for display and manipulation in the treeview """ self.layout_description.set_text(self.layout_descriptions[self.current_layout]) self.layout_treemodel.clear() # Display names for the widget ids names = { 'box': _('box'), 'notes': _('notes'), 'current': _('current slide'), 'next': _('next slide(s)'), 'highlight': _('highlighting'), 'annotations': _('annotations (hideable)'), 'vertical': _('vertical'), 'horizontal': _('horizontal'), } next_count = self.next_frames_action.get_state().get_int64() hltools_orientation = self.hltools_orientation_action.get_state().get_string() dfs_info = [(None, self.config.get_layout(self.current_layout))] while dfs_info: it, node = dfs_info.pop() if type(node) is str: orientation = names[hltools_orientation] if node == 'highlight' else '' next_slides = next_count if node == 'next' else 0 self.layout_treemodel.append(it, [node, False, None, bool(orientation), orientation, next_slides, names[node]]) else: next_it = self.layout_treemodel.append(it, ['box', True, node['resizeable'], True, names[node['orientation']], 0, names['box']]) dfs_info.extend((next_it, child) for child in reversed(node['children'])) self.layout_treeview.expand_all() def set_current_layout(self, layout): """ Update which is the layout currently used by the UI Args: layout (`str`): the layout id """ self.current_layout = layout def layout_selected(self, widget, event=None): """ Manage events for the layout selector drop-down menu Args: widget (:class:`~Gtk.ComboBox`): the widget which has been modified event (:class:`~Gdk.Event`): the GTK event """ self.current_layout = widget.get_active_id() self.load_layout() def get_info(self, path): """ Given a path string, look up the appropriate item in both the actual and GtkStore models Args: path (`str`): A string representing a path in the treemodel Returns: `dict`, :class:`~Gtk.TreeIter`: the node and iterator representing the position in the layout and model """ pos = Gtk.TreePath.new_from_string(path) tree_it = self.layout_treemodel.get_iter(pos) node = {'children': [self.config.get_layout(self.current_layout)]} for n in pos.get_indices(): node = node['children'][n] return node, tree_it def resizeable_toggled(self, widget, path): """ Handle when box’ resizeable value is toggled Args: widget (:class:`~Gtk.ComboBox`): the widget which has been modified path (`str`): A string representing the path to the modfied item """ node, tree_it = self.get_info(path) value = not node['resizeable'] node['resizeable'] = value self.layout_treemodel.set_value(tree_it, self._model_columns.index('resizeable'), value) self.normalize_layout(reload=False) def orientation_changed(self, widget, path, orient_it): """ Handle when the orientation of a box is changed Args: widget (:class:`~Gtk.ComboBox`): the widget which has been modified path (`str`): A string representing the path to the modfied item orient_it (:class:`~Gtk.TreeIter`): the row of the newly selected value in the orientations liststore model """ value = self.orientations_model.get_value(orient_it, 1) node, tree_it = self.get_info(path) if node == 'highlight': self.hltools_orientation_action.activate(GLib.Variant.new_string(value)) else: node['orientation'] = value self.layout_treemodel.set_value(tree_it, self._model_columns.index('orientation'), value) self.normalize_layout(reload=False) def next_slide_count_edited(self, widget, path, value): """ Handle when the next slide count is modified Args: widget (:class:`~Gtk.ComboBox`): the widget which has been modified path (`str`): A string representing the path to the modfied item value (`int`): the new number of next slides """ node, tree_it = self.get_info(path) self.layout_treemodel.set_value(tree_it, self._model_columns.index('next_slide_count'), int(value)) self.next_frames_action.activate(GLib.Variant.new_int64(int(value))) def treemodel_to_tree(self, iterator, parent_horizontal=False, parent_resizeable=False): """ Recursive function to transform the treemodel back into our dict-based representation of the layout Args: iterator (:class:`~Gtk.TreeIter`): the position in the treemodel parent_horizontal (`bool`): whether the parent node is horizontal parent_resieable (`bool`): whether the parent node is resizeable Returns: `list`: the list of `dict` or `str` representing the widgets at this level """ nodes = [] while iterator is not None: values = self.layout_treemodel.get(iterator, *range(len(self._model_columns[:-2]))) node = dict(zip(self._model_columns, values)) # Make the node conform to either a string or a dictionary with 'children' key if node.pop('has_resizeable'): node['children'] = [] del node['widget'] else: node = node['widget'] if self.layout_treemodel.iter_has_child(iterator): children = self.treemodel_to_tree(self.layout_treemodel.iter_children(iterator), *( [parent_horizontal, parent_resizeable] if type(node) is str else [node['orientation'] == 'horizontal', node['resizeable']] )) if len(children) > 1 and type(node) is not str: # Only assign children if there are any, allows to prune empty boxes node['children'] = children elif children and type(node) is not str: # Single-child box replaced by its children node = children[0] elif children: # Non-box node with children: create a new box and set the non-box as first child node = {'children': [node] + children, 'resizeable': not parent_resizeable, 'orientation': 'vertical' if parent_horizontal else 'horizontal'} # Only append widgets, and box nodes that have children if type(node) is str or node['children']: nodes.append(node) iterator = self.layout_treemodel.iter_next(iterator) return nodes def normalize_layout(self, widget=None, drag_context=None, reload=True): """ Handler at the end of a drag-and-drop in the treeview Here we transform the listmodel modified by drag-and-drop back to a valid `dict` and `str` hierarchy, and then trigger the loading of the layout again to display the corrected layout. Args: widget (:class:`~Gtk.Widget`): The object which received the signal drag_context (:class:`~Gdk.DragContext`): the drag context reload (`bool`): whether to reload the layout into the treemodel """ layout = self.treemodel_to_tree(self.layout_treemodel.get_iter_first()) if len(layout) > 1: layout = {'children': layout, 'orientation': 'horizontal', 'resizeable': True} else: layout = layout[0] # This validates self.config.update_layout_tree(self.current_layout, layout) self.ui_load_layout(None) if reload: self.load_layout() def show_editor(self, gaction, param=None): """ Show the popup to edit the layout. Gather info to populate it, and handle apply/cancel at the end. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ restore_layouts = {layout: copy.deepcopy(self.config.get_layout(layout)) for layout in self.layout_descriptions} self.next_frames_action = self.get_application().lookup_action('next-frames') self.hltools_orientation_action = self.get_application().lookup_action('highlight-tools-orientation') restore_next_count = self.next_frames_action.get_state().get_int64() restore_hltools_orientation = self.hltools_orientation_action.get_state().get_string() self.layout_selector.set_active_id(self.current_layout) self.load_layout() if self.layout_dialog.run() != Gtk.ResponseType.APPLY: for layout_name, layout in restore_layouts.items(): self.config.update_layout_tree(layout_name, layout) self.next_frames_action.activate(GLib.Variant.new_int64(restore_next_count)) self.hltools_orientation_action.activate(GLib.Variant.new_string(restore_hltools_orientation)) self.ui_load_layout(None) self.layout_dialog.hide() class AutoPlay(builder.Builder): """ Widget and machinery to setup and play slides automatically, optionally in a loop """ #: A :class:`~Gtk.Dialog` to contain the layout edition dialog autoplay_dialog = None #: The :class:`~Gtk.SpinButton` for the lower page autoplay_spin_lower = None #: The :class:`~Gtk.SpinButton` for the upper page autoplay_spin_upper = None #: The :class:`~Gtk.SpinButton` for the transition between slides autoplay_spin_time = None #: The :class:`~Gtk.CheckButton` to loop autoplay_button_loop = None #: :class:`~Glib.Source` which is the source id of the periodic slide transition, or `None` if there is no autoplay source = None #: if the timeout has been paused, `int` which represents the number of milliseconds until the next page slide remain = None #: callback, to be connected to :func:`~pympress.ui.UI.goto_page` goto_page = lambda *args: None def __init__(self, parent): super(AutoPlay, self).__init__() self.load_ui('autoplay') self.autoplay_dialog.set_transient_for(parent.p_win) self.autoplay_dialog.add_button(Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY) self.autoplay_dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) self.autoplay_dialog.set_default_response(Gtk.ResponseType.APPLY) parent.setup_actions({ 'autoplay': dict(activate=self.run), }) self.connect_signals(self) self.goto_page = parent.get_callback_handler('goto_page') def set_doc_pages(self, n_pages): """ Callback for when a document number of pages changes Args: n_pages (`int`): the number of pages of the loaded document """ self.autoplay_spin_lower.set_range(1, n_pages - 2) self.autoplay_spin_lower.set_value(1) self.autoplay_spin_upper.set_range(2, n_pages) self.autoplay_spin_upper.set_value(n_pages) def page_changed(self, spin_button, scroll_direction): """ Callback for when a page spin button is modified, maintains a delta of at least 2 pages between first and last page of the intended loop. (No loops needed to loop a single slide.) Args: spin_button (:class:`~Gtk.SpinButton`): The button whose value was changed scroll_direction (:class:`~Gtk.ScrollType`): The speed and amount of change """ if spin_button == self.autoplay_spin_lower: minval = self.autoplay_spin_lower.get_value() + 2 if self.autoplay_spin_upper.get_value() < minval: self.autoplay_spin_upper.set_value(minval) elif spin_button == self.autoplay_spin_upper: maxval = self.autoplay_spin_upper.get_value() - 2 if self.autoplay_spin_lower.get_value() > maxval: self.autoplay_spin_lower.set_value(maxval) def pause(self): """ Pause the looping if it’s running """ if self.source is None or self.remain is not None: return self.remain = self.source.get_ready_time() - self.source.get_time() self.source.set_ready_time(sys.maxsize) def unpause(self): """ Unpause the looping if it’s paused """ if self.source is None or self.remain is None: return self.source.set_ready_time(self.source.get_time() + self.remain) self.remain = None def is_looping(self): """ Return whether an auto-playing """ return self.source is not None def stop_looping(self): """ Stop the auto-playing """ if self.source is not None: self.source.destroy() self.source = None self.remain = None def start_looping(self): """ Start the auto-playing """ self.stop_looping() it = itertools.cycle(range(*self.pages[:2])) if self.pages[2] else iter(range(*self.pages[:2])) self.next_page(it) self.source = GLib.timeout_source_new(self.pages[3]) self.source.attach(GLib.MainContext.default()) self.source.set_callback(self.next_page, it) def next_page(self, it): """ Callback to turn the page to the next slide Args: it (`iterator`): An iterator that contains the next pages to load. Stop when there are no more pages. Returns: `bool`: `True` if he callback needs to be called again, otherwise `False` """ try: self.goto_page(next(it), autoplay=True) except StopIteration: self.stop_looping() return False else: return True def get_page_range(self): """ Return the autoplay info Returns: `tuple`: (first page, stop page, looping, delay i ms) """ return self.pages def run(self, gaction, param=None): """ Show the dialog to setup auto-play, and start the autoplay if « apply » is selected Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ reply = self.autoplay_dialog.run() self.autoplay_dialog.hide() if reply != Gtk.ResponseType.APPLY: return self.pages = (self.autoplay_spin_lower.get_value_as_int() - 1, self.autoplay_spin_upper.get_value_as_int(), self.autoplay_button_loop.get_active(), int(self.autoplay_spin_time.get_value() * 1000)) self.start_looping() pympress-1.7.1/pympress/document.py000066400000000000000000001307361415371354200174730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # document.py # # Copyright 2009, 2010 Thomas Jost # Copyright 2015 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.document` -- document handling --------------------------------------------- This module contains several classes that are used for managing documents (only PDF documents are supported at the moment, but other formats may be added in the future). An important point is that this module is *completely* independent from the GUI: there should not be any GUI-related code here, except for page rendering (and only rendering itself: the preparation of the target surface must be done elsewhere). """ import logging logger = logging.getLogger(__name__) import math import enum import pathlib import tempfile import mimetypes import webbrowser import collections from urllib.request import url2pathname from urllib.parse import urlsplit import gi gi.require_version('Poppler', '0.18') from gi.repository import Poppler from pympress.util import fileopen def get_extension(mime_type): """ Returns a valid filename extension (recognized by python) for a given mime type. Args: mime_type (`str`): The mime type for which to find an extension Returns: `str`: A file extension used for the given mimetype """ if not mimetypes.inited: mimetypes.init() for ext in mimetypes.types_map: if mimetypes.types_map[ext] == mime_type: return ext class PdfPage(enum.IntEnum): """ Represents the part of a PDF page that we want to draw. """ #: No notes on PDF page, only falsy value NONE = 0 #: Full PDF page (without notes) FULL = 1 #: Bottom half of PDF page BOTTOM = 2 #: Top half of PDF page TOP = 3 #: Right half of PDF page RIGHT = 4 #: Left half of PDF page LEFT = 5 #: Full page + draw another page for notes, which is after the slides AFTER = 6 #: Complemntary of AFTER: for a notes page, the slide page is BEFORE by half a document BEFORE = 7 def complement(val): """ Return the enum value for the other part of the page. """ return PdfPage(val ^ 1) def scale(val): """ Return the enum value that does only scaling not shifting. """ return PdfPage(val | 1) def direction(val): """ Returns whether the pdf page/notes mode is horizontal or vertical. Returns: `str`: a string representing the direction that can be used as the key in the config section """ if val == PdfPage.LEFT or val == PdfPage.RIGHT: return 'horizontal' elif val == PdfPage.TOP or val == PdfPage.BOTTOM: return 'vertical' elif val == PdfPage.AFTER or val == PdfPage.BEFORE: return 'page number' else: return None def from_screen(val, x, y, x2 = None, y2 = None): """ Transform visible part of the page coordinates to full page coordinates. Pass 2 floats to transform coordinates, 4 to transform margins, i.e. the second pair of coordinates is taken from the opposite corner. Args: x (`float`): x coordinate on the screen, on a scale 0..1 y (`float`): y coordinate on the screen, on a scale 0..1 x2 (`float`): second x coordinate on the screen, from the other side, on a scale 0..1 y2 (`float`): second y coordinate on the screen, from the other side, on a scale 0..1 """ if val == PdfPage.RIGHT: page = ((1 + x) / 2., y) elif val == PdfPage.LEFT: page = (x / 2., y) elif val == PdfPage.BOTTOM: page = (x, (1 + y) / 2.) elif val == PdfPage.TOP: page = (x, y / 2.) else: page = (x, y) if x2 is None or y2 is None: return page else: return page + val.complement().from_screen(x2, y2) def to_screen(val, x, y, x2 = None, y2 = None): """ Transform full page coordinates to visible part coordinates. Pass 2 floats to transform coordinates, 4 to transform margins, i.e. the second pair of coordinates is taken from the opposite corner. Args: x (`float`): x coordinate on the page, on a scale 0..1 y (`float`): y coordinate on the page, on a scale 0..1 x2 (`float`): second x coordinate on the page, from the other side, on a scale 0..1 y2 (`float`): second y coordinate on the page, from the other side, on a scale 0..1 """ if val == PdfPage.RIGHT: screen = (x * 2 - 1, y) elif val == PdfPage.LEFT: screen = (x * 2, y) elif val == PdfPage.BOTTOM: screen = (x, y * 2 - 1) elif val == PdfPage.TOP: screen = (x, y * 2) else: screen = (x, y) if x2 is None or y2 is None: return screen else: return screen + val.complement().to_screen(x2, y2) class Link(object): """ This class encapsulates one hyperlink of the document. Args: x1 (`float`): first x coordinate of the link rectangle y1 (`float`): first y coordinate of the link rectangle x2 (`float`): second x coordinate of the link rectangle y2 (`float`): second y coordinate of the link rectangle action (`function`): action to perform when the link is clicked """ #: `float`, first x coordinate of the link rectangle x1 = None #: `float`, first y coordinate of the link rectangle y1 = None #: `float`, second x coordinate of the link rectangle x2 = None #: `float`, second y coordinate of the link rectangle y2 = None #: `function`, action to be perform to follow this link follow = lambda *args, **kwargs: logger.error(_("no action defined for this link!")) def __init__(self, x1, y1, x2, y2, action): self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2 self.follow = action def is_over(self, x, y): """ Tell if the input coordinates are on the link rectangle. Args: x (`float`): input x coordinate y (`float`): input y coordinate Returns: `bool`: `True` if the input coordinates are within the link rectangle, `False` otherwise """ return ((self.x1 <= x) and (x <= self.x2) and (self.y1 <= y) and (y <= self.y2)) @staticmethod def build_closure(fun, *args, **kwargs): r""" Return a lambda that calls fun(\*args, \**kwargs), with the current value of args and kwargs. By creating the lambda in a new scope, we bind the arguments. Args: fun (`function`): The function to be called args (`tuple`): non-keyworded variable-length argument list to pass to fun() kwargs (`dict`): keyworded variable-length argument dict to pass to fun() """ return lambda *a, **k: fun(*(tuple(args) + tuple(a)), **dict(kwargs, **k)) #: A class that holds all the properties for media files Media = collections.namedtuple('Media', ['relative_margins', 'filename', 'autoplay', 'repeat', 'poster', 'show_controls'], defaults=[False, False, False, False]) class Page(object): """ Class representing a single page. It provides several methods used by the GUI for preparing windows for displaying pages, managing hyperlinks, etc. Args: doc (:class:`~Poppler.Page`): the poppler object around the page number (`int`): number of the page to fetch in the document parent (:class:`~pympress.document.Document`): the parent Document class """ #: Page handled by this class (instance of :class:`~Poppler.Page`) page = None #: `int`, number of the current page (starting from 0) page_nb = -1 #: `str` representing the page label page_label = None #: All the links in the page, as a `list` of :class:`~pympress.document.Link` instances links = [] #: All the media in the page, as a `list` of :class:`~pympress.document.Media` medias = [] #: `float`, page width pw = 0. #: `float`, page height ph = 0. #: All text annotations annotations = [] #: Instance of :class:`~pympress.document.Document` that contains this page. parent = None def __init__(self, page, number, parent): self.page = page self.page_nb = number self.parent = parent self.links = [] self.medias = [] self.annotations = [] if self.page is None: return # Get page label self.page_label = self.page.get_label() # Read page size self.pw, self.ph = self.page.get_size() # Read links on the page for link in self.page.get_link_mapping(): action = self.get_link_action(link.action.type, link.action) my_link = Link(link.area.x1, link.area.y1, link.area.x2, link.area.y2, action) self.links.append(my_link) # Read annotations, in particular those that indicate media for annotation in self.page.get_annot_mapping(): annot_type = annotation.annot.get_annot_type() if annot_type == Poppler.AnnotType.LINK: # just an Annot, not subclassed -- probably redundant with links continue elif annot_type == Poppler.AnnotType.MOVIE: movie = annotation.annot.get_movie() filepath = self.parent.get_full_path(movie.get_filename()) if not filepath: logger.error(_("Pympress can not find file ") + movie.get_filename()) continue relative_margins = Poppler.Rectangle() relative_margins.x1 = annotation.area.x1 / self.pw # left relative_margins.x2 = 1.0 - annotation.area.x2 / self.pw # right relative_margins.y1 = annotation.area.y1 / self.ph # bottom relative_margins.y2 = 1.0 - annotation.area.y2 / self.ph # top movie_options = {'show_controls': movie.show_controls(), 'poster': movie.need_poster()} try: movie_options['repeat'] = movie.get_play_mode() == Poppler.MoviePlayMode.REPEAT # NB: autoplay not part of Poppler’s MovieActivationParameters struct except AttributeError: pass # Missing functions in pre-21.04 Poppler versions media = Media(relative_margins, filepath, **movie_options) self.medias.append(media) action = Link.build_closure(self.parent.play_media, hash(media)) elif annot_type == Poppler.AnnotType.SCREEN: action_obj = annotation.annot.get_action() if not action_obj: continue action = self.get_annot_action(action_obj.any.type, action_obj, annotation.area) if not action: continue elif annot_type == Poppler.AnnotType.FILE_ATTACHMENT: attachment = annotation.annot.get_attachment() filename = pathlib.Path(attachment.name) with tempfile.NamedTemporaryFile('wb', suffix=filename.suffix, prefix=filename.stem, delete=False) as f: # now the file name is shotgunned filename = pathlib.Path(f.name) self.parent.remove_on_exit(filename) if not attachment.save(str(filename)): logger.error(_("Pympress can not extract attached file")) continue action = Link.build_closure(fileopen, filename) elif annot_type in {Poppler.AnnotType.TEXT, Poppler.AnnotType.POPUP, Poppler.AnnotType.FREE_TEXT}: # text-only annotations, hide them from screen and show them in annotations popup content = annotation.annot.get_contents() if content: self.annotations.append(annotation.annot) self.page.remove_annot(annotation.annot) continue elif annot_type in {Poppler.AnnotType.STRIKE_OUT, Poppler.AnnotType.HIGHLIGHT, Poppler.AnnotType.UNDERLINE, Poppler.AnnotType.SQUIGGLY, Poppler.AnnotType.POLYGON, Poppler.AnnotType.POLY_LINE, Poppler.AnnotType.SQUARE, Poppler.AnnotType.CIRCLE, Poppler.AnnotType.CARET, Poppler.AnnotType.LINE, Poppler.AnnotType.STAMP, Poppler.AnnotType.INK}: # Poppler already renders annotation of these types, nothing more can be done # even though the rendering isn't always perfect. continue else: logger.warning(_("Pympress can not interpret annotation of type:") + " {} ".format(annot_type)) continue my_annotation = Link(annotation.area.x1, annotation.area.y1, annotation.area.x2, annotation.area.y2, action) self.links.append(my_annotation) def get_link_action(self, link_type, action): """ Get the function to be called when the link is followed. Args: link_type (:class:`~Poppler.ActionType`): The type of action to be performed action (:class:`~Poppler.Action`): The atcion to be performed Returns: `function`: The function to be called to follow the link """ # Poppler.ActionType.RENDITION should only appear in annotations, right? Otherwise how do we know # where to render it? Any documentation on which action types are admissible in links vs in annots # is very welcome. For now, link is fallback to annot so contains all action types. if link_type == Poppler.ActionType.NONE: return lambda: None elif link_type == Poppler.ActionType.GOTO_DEST: dest_type = action.goto_dest.dest.type if dest_type == Poppler.DestType.NAMED: dest = self.parent.doc.find_dest(action.goto_dest.dest.named_dest) if dest: return Link.build_closure(self.parent.goto_page, dest.page_num - 1) else: warning = _('Unrecognized named destination: ') + str(action.goto_dest.dest.named_dest) elif dest_type != Poppler.DestType.UNKNOWN: return Link.build_closure(self.parent.goto_page, action.goto_dest.dest.page_num - 1) elif link_type == Poppler.ActionType.NAMED: dest_name = action.named.named_dest dest = self.parent.doc.find_dest(dest_name) if dest: return Link.build_closure(self.parent.goto_page, dest.page_num) elif dest_name == "GoBack": return self.parent.goto_prev_hist elif dest_name == "GoForward": return self.parent.goto_next_hist elif dest_name == "FirstPage": return Link.build_closure(self.parent.goto_page, 0) elif dest_name == "PrevPage": return Link.build_closure(self.parent.goto_page, self.page_nb - 1) elif dest_name == "NextPage": return Link.build_closure(self.parent.goto_page, self.page_nb + 1) elif dest_name == "LastPage": return Link.build_closure(self.parent.goto_page, self.parent.pages_number() - 1) elif dest_name == "GoToPage": # Same as the 'G' action which allows one to pick a page to jump to return Link.build_closure(self.parent.start_editing_page_number, ) elif dest_name == "Find": # TODO popup a text box and search results with Page.find_text # http://lazka.github.io/pgi-docs/Poppler-0.18/classes/Page.html#Poppler.Page.find_text warning = _("Pympress does not yet support link type \"{}\" to \"{}\"").format(link_type, dest_name) else: # TODO find out other possible named actions? warning = _("Pympress does not recognize link type \"{}\" to \"{}\"").format(link_type, dest_name) elif link_type == Poppler.ActionType.LAUNCH: launch = action.launch if launch.params: logger.warning("ignoring params: " + str(launch.params)) filepath = self.parent.get_full_path(launch.file_name) if not filepath: logger.error("can not find file " + launch.file_name) return lambda: None else: return Link.build_closure(fileopen, filepath) elif link_type == Poppler.ActionType.URI: return Link.build_closure(webbrowser.open_new_tab, action.uri.uri) elif link_type == Poppler.ActionType.RENDITION: # Poppler 0.22 warning = _("Pympress does not yet support link type \"{}\"").format(link_type) elif link_type == Poppler.ActionType.MOVIE: # Poppler 0.20 warning = _("Pympress does not yet support link type \"{}\"").format(link_type) elif link_type == Poppler.ActionType.GOTO_REMOTE: warning = _("Pympress does not yet support link type \"{}\"").format(link_type) elif link_type == Poppler.ActionType.OCG_STATE: warning = _("Pympress does not yet support link type \"{}\"").format(link_type) elif link_type == Poppler.ActionType.JAVASCRIPT: warning = _("Pympress does not yet support link type \"{}\"").format(link_type) elif link_type == Poppler.ActionType.UNKNOWN: warning = _("Pympress does not yet support link type \"{}\"").format(link_type) else: warning = _("Pympress does not recognize link type \"{}\"").format(link_type) logger.info(warning) return Link.build_closure(logger.warning, _('Unsupported link clicked. ') + warning) def get_annot_action(self, link_type, action, rect): """ Get the function to be called when the link is followed. Args: link_type (:class:`~Poppler.ActionType`): The link type action (:class:`~Poppler.Action`): The action to be performed when the link is clicked rect (:class:`~Poppler.Rectangle`): The region of the page where the link is Returns: `function`: The function to be called to follow the link """ if link_type == Poppler.ActionType.RENDITION: media = action.rendition.media if media.is_embedded(): ext = get_extension(media.get_mime_type()) with tempfile.NamedTemporaryFile('wb', suffix=ext, prefix='pdf_embed_', delete=False) as f: # now the file name is shotgunned filename = pathlib.Path(f.name) self.parent.remove_on_exit(filename) if not media.save(str(filename)): logger.error(_("Pympress can not extract embedded media")) return None else: filename = self.parent.get_full_path(media.get_filename()) if not filename: logger.error(_("Pympress can not find file ") + media.get_filename()) return None relative_margins = Poppler.Rectangle() relative_margins.x1 = rect.x1 / self.pw # left relative_margins.x2 = 1.0 - rect.x2 / self.pw # right relative_margins.y1 = rect.y1 / self.ph # bottom relative_margins.y2 = 1.0 - rect.y2 / self.ph # top media_options = {} try: media_options['autoplay'] = media.get_auto_play() media_options['show_controls'] = media.get_show_controls() media_options['repeat'] = media.get_repeat_count() - 1 # NB: no poster in Poppler’s MediaParameters except AttributeError: pass media = Media(relative_margins, filename, **media_options) self.medias.append(media) return Link.build_closure(self.parent.play_media, hash(media)) else: return self.get_link_action(link_type, action) def number(self): """ Get the page number. """ return self.page_nb def label(self): """ Get the page label. """ return self.page_label def get_link_at(self, x, y, dtype=PdfPage.FULL): """ Get the :class:`~pympress.document.Link` corresponding to the given position. Returns `None` if there is no link at this position. Args: x (`float`): horizontal coordinate y (`float`): vertical coordinate dtype (:class:`~pympress.document.PdfPage`): the type of document to consider Returns: :class:`~pympress.document.Link`: the link at the given coordinates if one exists, `None` otherwise """ x, y = dtype.from_screen(x, y) xx = self.pw * x yy = self.ph * (1. - y) for link in self.links: if link.is_over(xx, yy): return link return None def get_size(self, dtype=PdfPage.FULL): """ Get the page size. Args: dtype (:class:`~pympress.document.PdfPage`): the type of document to consider Returns: `(float, float)`: page size """ return dtype.scale().from_screen(self.pw, self.ph) def get_aspect_ratio(self, dtype=PdfPage.FULL): """ Get the page aspect ratio. Args: dtype (:class:`~pympress.document.PdfPage`): the type of document to consider Returns: `float`: page aspect ratio """ w, h = self.get_size(dtype) return w / h def get_annotations(self): """ Get the list of text annotations on this page. Returns: `list` of `str`: annotations on this page """ return self.annotations def new_annotation(self, pos, rect=None): """ Add an annotation to this page Args: pos (`int`): The position in the list of annotations in which to insert this annotation rect (:class:`~Poppler.Rectangle`): A rectangle for the position of this annotation Returns: :class:`~Poppler.Annot`: A new annotation on this page """ if pos < 0: pos = 0 if pos > len(self.annotations): pos = len(self.annotations) if rect is None: rect = Poppler.Rectangle() rect.x1 = self.pw - 20 rect.x2 = rect.x1 + 20 rect.y2 = self.ph - len(self.annotations) * 20 rect.y1 = rect.y2 - 20 new_annot = Poppler.AnnotText.new(self.parent.doc, rect) new_annot.set_icon(Poppler.ANNOT_TEXT_ICON_NOTE) self.annotations.insert(pos, new_annot) self.parent.made_changes() return new_annot def set_annotation(self, pos, value): """ Add an annotation to this page Args: pos (`int`): The number of the annotation value (`str`): The new contents of the annotation """ rect = self.annotations[pos].get_rectangle() self.remove_annotation(pos) self.new_annotation(pos, rect).set_contents(value) def remove_annotation(self, pos): """ Add an annotation to this page Args: pos (`int`): The number of the annotation """ self.parent.made_changes() del self.annotations[pos] def get_media(self): """ Get the list of medias this page might want to play. Returns: `list`: medias in this page """ return self.medias def render_cairo(self, cr, ww, wh, dtype=PdfPage.FULL): """ Render the page on a Cairo surface. Args: cr (:class:`~Gdk.CairoContext`): target surface ww (`int`): target width in pixels wh (`int`): target height in pixels dtype (:class:`~pympress.document.PdfPage`): the type of document that should be rendered """ pw, ph = self.get_size(dtype) cr.set_source_rgb(1, 1, 1) # Scale scale = min(ww / pw, wh / ph) cr.scale(scale, scale) cr.rectangle(0, 0, pw, ph) cr.fill() # For "regular" pages, there is no problem: just render them. # For other pages (i.e. half of a page), the widget already has correct # dimensions so we don't need to deal with that. But for right and bottom # halfs we must translate the output in order to only show the correct half. if dtype == PdfPage.RIGHT: cr.translate(-pw, 0) elif dtype == PdfPage.BOTTOM: cr.translate(0, -ph) self.page.render(cr) def can_render(self): """ Informs that rendering *is* necessary (avoids checking the type). Returns: `bool`: `True`, do rendering """ return True class Document(object): """ This is the main document handling class. .. note:: The internal page numbering scheme is the same as in Poppler: it starts at 0. Args: builder (:class:`pympress.builder.Builder`): A builder to load callbacks pop_doc (:class:`~pympress.Poppler.Document`): Instance of the Poppler document that this class will wrap uri (`str`): URI of the PDF file to open page (`int`): page number to which the file should be opened """ #: Current PDF document (:class:`~Poppler.Document` instance) doc = None #: `str` full path to pdf uri = None #: :class:`~pathlib.Path` to pdf if uri is a file: URI path = None #: Number of pages in the document nb_pages = -1 #: Pages cache (`dict` of :class:`~pympress.document.Page`). This makes #: navigation in the document faster by avoiding calls to Poppler when loading #: a page that has already been loaded. pages_cache = {} #: `set` of :class:`~pathlib.Path` representing the temporary files which need to be removed temp_files = set() #: History of pages we have visited history = [] #: Our position in the history hist_pos = -1 #: `dict` of all the page labels page_labels = [] #: `bool` indicating whether the second half of pages are in fact notes pages notes_after = False #: `bool` indicating whether there were modifications to the document changes = False #: callback, to be connected to :func:`~pympress.extras.Media.play` play_media = lambda *args: None #: callback, to be connected to :func:`~pympress.editable_label.PageNumber.start_editing` start_editing_page_number = lambda *args: None #: callback, to be connected to :func:`~pympress.ui.UI.goto_page` navigate = lambda *args: None def __init__(self, builder, pop_doc, uri): if builder is not None: # Connect callbacks self.play_media = builder.get_callback_handler('medias.play') self.start_editing_page_number = builder.get_callback_handler('page_number.start_editing') self.goto_page = builder.get_callback_handler('goto_page') self.goto_next_hist = builder.get_callback_handler('doc_hist_next') self.goto_prev_hist = builder.get_callback_handler('doc_hist_prev') # Setup PDF file self.uri = uri self.doc = pop_doc self.changes = False if uri is not None: uri_parts = urlsplit(uri, scheme='file') self.path = pathlib.Path(url2pathname(uri_parts.path)) if uri_parts.scheme == 'file': self.path = pathlib.Path.cwd().joinpath(self.path.name) else: self.path = None # Pages number if pop_doc is not None: self.nb_pages = self.doc.get_n_pages() self.page_labels = [self.doc.get_page(n).get_label() for n in range(self.nb_pages)] else: self.nb_pages = 0 self.page_labels = [] # Pages cache self.pages_cache = {} def get_structure(self, index_iter = None): """ Gets the structure of the document from its index. Recursive, pass the iterator. Args: index_iter (:class:`~Poppler.IndexIter` or `None`): the iterator for the child index to explore. Returns: `list`: A list of tuples (depth, page number, title) """ try: if index_iter is None: index_iter = Poppler.IndexIter(self.doc) except TypeError: return {} if index_iter is None: return {} index = {} while True: action = index_iter.get_action() title = '' page = None try: if action.type == Poppler.ActionType.GOTO_DEST: title = action.goto_dest.title if action.goto_dest.dest.type == Poppler.DestType.NAMED: page = self.doc.find_dest(action.goto_dest.dest.named_dest).page_num - 1 elif action.goto_dest.dest.type == Poppler.DestType.UNKNOWN: raise AssertionError('Unknown type of destination') else: page = action.goto_dest.dest.page_num - 1 else: raise AssertionError('Unexpected type of action') except Exception: logger.error(_('Unexpected action in index "{}"').format(action.type)) page = None new_entry = {'title': title} child = index_iter.get_child() if child: new_entry['children'] = self.get_structure(child) if page is None: page = min(new_entry['children']) # there should not be synonymous sections, correct the page here to a better guess if page in index: lower_bound = max(index.keys()) find = index[lower_bound] while 'children' in find: lower_bound = max(find['children'].keys()) find = find['children'][lower_bound] try: page = min(number for number, label in enumerate(self.page_labels) if label == self.page_labels[page] and number > lower_bound) except ValueError: # empty iterator page = lower_bound + 1 if page is not None: index[page] = new_entry if not index_iter.next(): break return index @staticmethod def create(builder, uri): """ Initializes a Document by passing it a :class:`~Poppler.Document`. Args: builder (:class:`pympress.builder.Builder`): A builder to load callbacks uri (`str`): URI to the PDF file to open page (`int`): page number to which the file should be opened Returns: :class:`~pympress.document.Document`: The initialized document """ if uri is None: doc = EmptyDocument() else: poppler_doc = Poppler.Document.new_from_file(uri, None) doc = Document(builder, poppler_doc, uri) return doc def made_changes(self): """ Notify the document that some changes were made (e.g. annotations edited) """ self.changes = True def has_changes(self): """ Return whether that some changes were made (e.g. annotations edited) """ return self.changes def save_changes(self, dest_uri=None): """ Save the changes Args: dest_uri (`str` or `None`): The URI where to save the file, or None to save in-place """ if self.doc is None: return for page in self.pages_cache.values(): for annot in page.get_annotations(): page.page.add_annot(annot) if dest_uri is not None and dest_uri != self.uri: if self.doc.save(dest_uri): self.changes = False else: # We can’t overwrite the current file directly, so create a temporary file and then overwrite with tempfile.NamedTemporaryFile('wb', suffix=self.path.suffix, prefix=self.path.stem, dir=self.path.parent, delete=False) as f: temp_path = pathlib.Path(f.name) if self.doc.save(temp_path.as_uri()): self.changes = False temp_path.replace(self.path) for page in self.pages_cache.values(): for annot in page.get_annotations(): page.page.remove_annot(annot) def guess_notes(self, horizontal, vertical, current_page=0): """ Get our best guess for the document mode. Args: horizontal (`str`): A string representing the preference for horizontal slides vertical (`str`): A string representing the preference for vertical slides Returns: :class:`~pympress.document.PdfPage`: the notes mode """ page = self.page(current_page) if page is None or not page.can_render(): return PdfPage.NONE ar = page.get_aspect_ratio() # Check whether we have N slides with one aspect ratio then N slides with a different aspect ratio # that is the sign if Libreoffice notes pages if self.nb_pages and self.nb_pages % 2 == 0: half_doc = self.nb_pages // 2 ar_slides = self.page(0).get_aspect_ratio() ar_notes = self.page(half_doc).get_aspect_ratio() if ar_slides != ar_notes and \ all(self.page(p).get_aspect_ratio() == ar_slides for p in range(1, half_doc)) and \ all(self.page(half_doc + p).get_aspect_ratio() == ar_notes for p in range(1, half_doc)): return PdfPage.AFTER # "Regular" slides will have an aspect ratio of 4/3, 16/9, 16/10... i.e. in the range [1..2] # So if the aspect ratio is >= 2, we can assume it is a document with notes on the side. if ar >= 2: try: return PdfPage[horizontal.upper()] except KeyError: return PdfPage.RIGHT # Make exception for classic american letter format and ISO (A4, B5, etc.) if abs(ar - 8.5 / 11) < 1e-3 or abs(ar - 1 / math.sqrt(2)) < 1e-3: return PdfPage.NONE # If the aspect ratio is < 1, we can assume it is a document with notes above or below. if ar < 1: try: return PdfPage[vertical.upper()] except KeyError: return PdfPage.BOTTOM return PdfPage.NONE def set_notes_after(self, notes_after): """ Set whether there are notes pages after normal pages (aka Libreoffice notes mode) Args: notes_after (`bool`): Whether there are notes pages """ self.notes_after = notes_after def page(self, number): """ Get the specified page. Args: number (`int`): number of the page to return Returns: :class:`~pympress.document.Page`: the wanted page, or `None` if it does not exist """ if number >= self.pages_number() or number < 0: return None if number not in self.pages_cache: self.pages_cache[number] = Page(self.doc.get_page(number), number, self) return self.pages_cache[number] def notes_page(self, number): """ Get the specified page. Args: number (`int`): number of the page to return Returns: :class:`~pympress.document.Page`: the wanted page, or `None` if it does not exist """ if number >= self.pages_number() or number < 0: return None if self.notes_after: number = number + self.pages_number() if number not in self.pages_cache: self.pages_cache[number] = Page(self.doc.get_page(number), number, self) return self.pages_cache[number] def pages_number(self): """ Get the number of pages in the document. Returns: `int`: the number of pages in the document """ return (self.nb_pages // 2) if self.notes_after else self.nb_pages def has_labels(self): """ Return whether this document has useful labels. Returns: `bool`: False iff there are no labels or they are just the page numbers """ return self.page_labels != [str(n + 1) for n in range(self.nb_pages)] def lookup_label(self, label, prefix_unique=True): """ Find a page from its label. Args: label (`str`): the label we are searching for prefix_unique (`bool`): whether a prefix match should be unique, e.g. when the user is still typing Returns: `int`: the page """ # somehow this always returns None: # page = self.doc.get_page_by_label(label).get_index() # make a shortlist: squash synonymous labels, keeping the last one compatible_labels = {l: n for n, l in enumerate(self.page_labels) if l.lower().startswith(label.lower())} if len(compatible_labels) == 1: return set(compatible_labels.values()).pop() # try exact match try: return compatible_labels[label] except KeyError: pass # try case-insensitive match, prefix case-sensitive match, prefix case-insensitive match (unless prefix_unique) full = len(label) for filtering in [lambda l: len(l) == full, lambda l: l.startswith(label), lambda l: not prefix_unique]: try: found = next(label for label in compatible_labels if filtering(label)) except StopIteration: pass else: return compatible_labels[found] return None def goto(self, number): """ Switch to another page. Args: number (`int`): number of the destination page """ if number < 0: number = 0 if number >= self.pages_number(): number = self.pages_number() - 1 if 0 <= self.hist_pos < len(self.history) and self.history[self.hist_pos] == number: return number # chop off history where we were and append self.hist_pos = min(len(self.history), self.hist_pos + 1) del self.history[self.hist_pos:] self.history.append(number) return number def label_after(self, page): """ Switch to the next page with different label. If we're within a set of pages with the same label we want to go to the last one. """ labels_after = enumerate(self.page_labels[page + 1:], page + 1) try: next_page, next_label = next(labels_after) except StopIteration: # we're already at the last page! return page # will stop as soon as next_page + 1 (aka following_page) is a different label or due to end of iterator for following_page, following_label in labels_after: if following_label == next_label: next_page = following_page else: break return next_page def label_before(self, page): """ Switch to the previous page with different label. If we're within a set of pages with the same label we want to go *before* the first one. """ # will stop as soon as we find a different label or due to end of iterator for prev_page, prev_label in enumerate(reversed(self.page_labels[:page])): if prev_label != self.page_labels[page]: return page - 1 - prev_page return 0 def hist_next(self, *args): """ Switch to the page we viewed next. """ if self.hist_pos + 1 == len(self.history): return None self.hist_pos += 1 return self.history[self.hist_pos] def hist_prev(self, *args): """ Switch to the page we viewed before. """ if self.hist_pos == 0: return None self.hist_pos -= 1 return self.history[self.hist_pos] def get_uri(self): """ Gives access to the URI, rather than the path, of this document. Returns: `str`: the URI to the file currently opened. """ return self.uri def get_full_path(self, filename): """ Returns full path, extrapolated from a path relative to this document or to the current directory. Args: filename (:class:`~pathlib.Path` or `str`): Name of the file or relative path to it Returns: :class:`~pathlib.Path`: the full path to the file or None if it doesn't exist """ filename = pathlib.Path(filename) if filename.is_absolute(): return filename.resolve() if filename.exists() else None for dirname in [pathlib.Path(url2pathname(urlsplit(self.uri).path)).parent, pathlib.Path.cwd()]: filepath = dirname.joinpath(filename) if filepath.exists(): return filepath def remove_on_exit(self, filename): """ Remember a temporary file to delete later. Args: filename (:class:`~pathlib.Path`): The path to the file to delete """ self.temp_files.add(filename) def cleanup_media_files(self): """ Removes all files that were extracted from the pdf into the filesystem. """ for f in self.temp_files: if f.exists(): f.unlink() self.temp_files.clear() class EmptyPage(Page): """ A dummy page, placeholder for when there are no valid pages around. This page is a non-notes page with an aspect ratio of 1.3 and nothing else inside. Also, it has no "rendering" capability, and is made harmless by overriding its render function. """ def __init__(self): super(EmptyPage, self).__init__(None, -1, None) self.page_label = None # by default, anything that will have a 1.3 asapect ratio self.pw, self.ph = 1.3, 1.0 def render_cairo(self, cr, ww, wh, dtype=PdfPage.FULL): """ Overriding this purely for safety: make sure we do not accidentally try to render. Args: cr (:class:`~Gdk.CairoContext`): target surface ww (`int`): target width in pixels wh (`int`): target height in pixels dtype (:class:`~pympress.document.PdfPage`): the type of document that should be rendered """ pass def can_render(self): """ Informs that rendering is *not* necessary (avoids checking the type). Returns: `bool`: `False`, no rendering """ return False class EmptyDocument(Document): """ A dummy document, placeholder for when no document is open. """ def __init__(self): super(EmptyDocument, self).__init__(None, None, None) self.pages_cache[-1] = EmptyPage() def page(self, number): """ Retrieve a page from the document. Args: number (`int`): page number to be retrieved Returns: :class:`~pympress.document.EmptyPage` or `None`: -1 returns the empty page so we can display something. """ return self.pages_cache[number] if number in self.pages_cache else None def notes_page(self, number): """ Retrieve a page from the document. Args: number (`int`): page number to be retrieved Returns: :class:`~pympress.document.EmptyPage` or `None`: -1 returns the empty page so we can display something. """ return self.pages_cache[number] if number in self.pages_cache else None ## # Local Variables: # mode: python # indent-tabs-mode: nil # py-indent-offset: 4 # fill-column: 80 # end: pympress-1.7.1/pympress/editable_label.py000066400000000000000000000432501415371354200205570ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # editable_label.py # # Copyright 2017 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.editable_label` -- A label that can be swapped out for an editable entry --------------------------------------------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GLib, Gio class EditableLabel(object): """ A label that can switch between simply displaying a value, and allowing user input to edit this value. """ #: :class:`~Gtk.EventBox` around the label, used to sense clicks event_box = None #: `bool` tracking whether we are currently editing the label. editing = False def on_label_event(self, widget_or_action, event=None): """ Manage events on the current slide label/entry. This function triggers replacing the label with an entry when clicked or otherwise toggled. Args: widget (:class:`~Gtk.Widget`): the widget in which the event occurred event (:class:`~Gtk.Event` or None): the event that occurred, None if tf we called from a menu item Returns: `bool`: whether the event was consumed """ hint = None if issubclass(type(widget_or_action), Gio.Action): hint = widget_or_action.get_name() elif event is not None and event.type == Gdk.EventType.BUTTON_PRESS: # If we clicked on the Event Box then don't toggle, just enable. if widget_or_action is not self.event_box or self.editing: return False else: return False # Perform the state toggle if not self.editing: self.swap_label_for_entry(hint) else: self.validate() return True def validate(self): """ Validate the input to the label. Needs to be reimplemented by children classes. """ raise NotImplementedError def cancel(self): """ Cancel editing the label. Needs to be reimplemented by children classes. """ pass def on_keypress(self, widget, event): """ Manage key presses for the editable label. Needs to be reimplemented by children classes. If we are editing the label, intercept some key presses (to validate or cancel editing or other specific behaviour), otherwise pass the key presses on to the button for normal behaviour. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. name (`str`): the name of the key stroke command (`str`): the name of the command in case this function is called by on_navigation Returns: `bool`: whether the event was consumed """ raise NotImplementedError def try_cancel(self): """ Cancel editing the label, if it is being edited. Returns: `bool`: `True` if editing got cancelled, `False` if the label was not being edited. """ if not self.editing: return False self.cancel() self.restore_label() return True def try_validate(self): """ Validate the page choice, if the page label is being edited. Returns: `bool`: `True` if editing got validated, `False` if the label was not being edited. """ if not self.editing: return False self.validate() self.restore_label() return True def swap_label_for_entry(self): """ Perform the actual work of starting the editing. """ raise NotImplementedError def restore_label(self): """ Make sure that the editable label is not in entry mode. If it is an entry, then replace it with the label. """ raise NotImplementedError def start_editing(self): """ Start the editing of the label if it is disabled. """ if not self.editing: self.swap_label_for_entry() def stop_editing(self): """ Disable the editing of the label if it was enabled. """ if self.editing: self.restore_label() class PageNumber(EditableLabel): """ A label that displays "current page / max page", that can be edited to select a page to which to go. Args: builder (:class:`~pympress.builder.Builder`): A builder from which to load widgets """ #: Slide counter :class:`~Gtk.Label` for the current slide. label_cur = None #: Slide counter :class:`~Gtk.Label` for the last slide. label_last = None #: :class:`~Gtk.EventBox` associated with the slide counter label in the Presenter window. eb_cur = None #: :class:`~Gtk.HBox` containing the slide counter label in the Presenter window. hb_cur = None #: :class:`~Gtk.SpinButton` used to switch to another slide by typing its number. spin_cur = None #: :class:`~Gtk.Entry` used to switch to another slide by typing its label. edit_label = None #: :class:`~Gtk.Label` separating `~spin_cur` and `~edit_label` label_sep = None #: `int` holding the maximum page number in the document max_page_number = 1 #: `bool` holding whether we display or ignore page labels page_labels = True #: `bool` whether to scroll with the pages (True) or with the page numbers (False) invert_scroll = True #: callback, to be connected to :func:`~pympress.document.Document.goto` goto_page = lambda *args: None #: callback, to be connected to :func:`~pympress.document.Document.lookup_label` find_label = lambda *args: None #: callback, to be connected to :func:`~pympress.document.Document.label_after` label_before = lambda *args: None #: callback, to be connected to :func:`~pympress.document.Document.label_before` label_after = lambda *args: None #: callback, to be connected to :func:`~pympress.ui.UI.do_page_change` page_change = lambda *args: None def __init__(self, builder, page_num_scroll): super(PageNumber, self).__init__() # The spinner's scroll is with page numbers, invert to scroll with pages self.invert_scroll = not page_num_scroll builder.load_widgets(self) builder.setup_actions({ 'goto-page': dict(activate=self.on_label_event), 'jumpto-label': dict(activate=self.on_label_event), 'cancel-page-number': dict(activate=self.cancel), }) self.goto_page = builder.get_callback_handler('goto_page') self.page_change = builder.get_callback_handler('do_page_change') self.setup_doc_callbacks(builder.doc) # Initially (from XML) both the spinner and the current page label are visible. self.hb_cur.remove(self.spin_cur) self.hb_cur.remove(self.edit_label) self.hb_cur.remove(self.label_sep) self.event_box = self.eb_cur def setup_doc_callbacks(self, doc): """ Callbacks that need to be setup again at every new document Args: doc (:class:`~pympress.document.Document`): The new document that got loaded """ self.find_label = doc.lookup_label self.label_after = doc.label_after self.label_before = doc.label_before def set_last(self, num_pages): """ Set the max number of pages, both on display and as the range of values for the spinner. Args: num_pages (`int`): The maximum page number """ self.max_page_number = num_pages self.label_last.set_text(('/{})' if self.page_labels else '/{}').format(num_pages)) self.spin_cur.set_range(1, num_pages) self.spin_cur.set_max_length(len(str(num_pages)) + 1) def enable_labels(self, enable): """ Allow one to use or ignore labels. Args: enable (`bool`): Whether to enable labels """ self.page_labels = enable self.label_last.set_text(('/{})' if enable else '/{}').format(self.max_page_number)) def changed_page_label(self, *args): """ Get the page number from the spinner and go to that page. """ if not self.page_labels or not self.edit_label.is_focus() or not self.edit_label.get_text(): return page_nb = self.find_label(self.edit_label.get_text(), prefix_unique = True) if not page_nb: return # use the spinner's mechanism self.spin_cur.set_value(page_nb + 1) def validate(self): """ Get the page number from the spinner and go to that page. """ page_nb = None if self.page_labels and self.edit_label.is_focus(): page_nb = self.find_label(self.edit_label.get_text(), prefix_unique = False) if page_nb is None: page_nb = int(self.spin_cur.get_value() - 1) if page_nb is not None: self.restore_label() self.goto_page(page_nb) else: self.cancel() def cancel(self, gaction=None, param=None): """ Make the UI re-display the pages from before editing the current page. """ self.restore_label() GLib.idle_add(self.page_change, unpause=False) def on_keypress(self, widget, event): """ Implement directions (left/right/home/end) keystrokes. Otherwise pass on to :func:`~Gtk.SpinButton.do_key_press_event()`. """ if self.page_labels and self.edit_label.is_focus(): return Gtk.Entry.do_key_press_event(self.edit_label, event) else: return Gtk.SpinButton.do_key_press_event(self.spin_cur, event) def on_scroll(self, widget, event): """ Scroll event. Pass it on to the spin button if we're currently editing the page number. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. Returns: `bool`: whether the event was consumed """ if not self.editing: return False else: # flip scroll direction to get scroll down advancing slides if self.invert_scroll and event.direction == Gdk.ScrollDirection.DOWN: event.direction = Gdk.ScrollDirection.UP elif self.invert_scroll and event.direction == Gdk.ScrollDirection.UP: event.direction = Gdk.ScrollDirection.DOWN # Manually get destination slide if we're editing labels if self.edit_label.is_focus(): cur_page = int(self.spin_cur.get_value()) - 1 if event.direction == Gdk.ScrollDirection.DOWN: self.spin_cur.set_value(1 + self.label_before(cur_page)) elif event.direction == Gdk.ScrollDirection.UP: self.spin_cur.set_value(1 + self.label_after(cur_page)) # Otherwise let the spinner do its job else: return Gtk.SpinButton.do_scroll_event(self.spin_cur, event) def swap_label_for_entry(self, hint=None): """ Perform the actual work of starting the editing. """ Gio.Application.get_default().activate_action('cancel-talk-time') label, sep, cur = self.label_cur.get_text().rpartition('(') # Replace label with entry self.spin_cur.show() self.hb_cur.pack_start(self.spin_cur, True, True, 0) self.hb_cur.reorder_child(self.spin_cur, 1) if self.page_labels: self.hb_cur.pack_start(self.edit_label, True, True, 0) self.hb_cur.reorder_child(self.edit_label, 0) self.edit_label.set_text(label.strip()) self.hb_cur.pack_start(self.label_sep, True, True, 0) self.hb_cur.reorder_child(self.label_sep, 1) self.label_sep.set_text(' (') self.hb_cur.set_homogeneous(False) self.hb_cur.remove(self.label_cur) try: cur_nb = int(cur.strip()) except ValueError: cur_nb = -1 self.spin_cur.set_value(cur_nb) if self.page_labels and (hint == 'jumpto-label' or hint == 'nav_jump'): self.edit_label.grab_focus() self.edit_label.select_region(0, -1) else: self.spin_cur.grab_focus() self.spin_cur.select_region(0, -1) self.editing = True def restore_label(self): """ Make sure that the current page number is displayed in a label and not in an entry. If it is an entry, then replace it with the label. """ if self.spin_cur in self.hb_cur: if self.page_labels: self.hb_cur.set_homogeneous(True) self.hb_cur.remove(self.edit_label) self.hb_cur.remove(self.label_sep) self.hb_cur.remove(self.spin_cur) self.hb_cur.pack_start(self.label_cur, True, True, 0) self.hb_cur.reorder_child(self.label_cur, 0) self.editing = False def update_page_numbers(self, cur_nb, label): """ Update the displayed page numbers. Args: cur_nb (`int`): The current page number, in documentation numbering (range [0..max - 1]) label (`str`): The current page label """ cur = str(cur_nb + 1) if self.page_labels: self.label_cur.set_text('{} ({}'.format(str(label), cur)) else: self.label_cur.set_text(cur) if self.editing: if not self.edit_label.is_focus(): self.edit_label.set_text(str(label)) self.spin_cur.set_value(cur_nb + 1) class EstimatedTalkTime(EditableLabel): """ A label that displays the time elapsed since the start of the talk, that can be edited to select talk duration. The duration of the talk will cause the label to blink and change colour as the elapsed time gets closer to the targeted talk duration. Args: builder (builder.Builder): The builder from which to load widgets. """ #: Elapsed time :class:`~Gtk.Label`. label_time = None #: Estimated talk time :class:`~Gtk.Label` for the talk. label_ett = None #: :class:`~Gtk.EventBox` associated with the estimated talk time. eb_ett = None #: Estimated talk time, `int` in seconds. est_time = 0 #: :class:`~Gtk.Entry` used to set the estimated talk time. entry_ett = None #: callback, to be connected to :func:`~pympress.editable_label.PageNumber.stop_editing` stop_editing_page_number = lambda *args: None def __init__(self, builder): super(EstimatedTalkTime, self).__init__() self.entry_ett = Gtk.Entry() builder.load_widgets(self) builder.setup_actions({ 'edit-talk-time': dict(activate=self.on_label_event), 'set-talk-time': dict(activate=self.set_time, parameter_type=int), 'cancel-talk-time': dict(activate=self.restore_label), }) self.event_box = self.eb_ett def validate(self): """ Update estimated talk time from the input. """ text = self.entry_ett.get_text() t = ["0" + n.strip() for n in text.split(':')] try: m = int(t[0]) s = int(t[1]) except ValueError: logger.error(_("Invalid time (mm or mm:ss expected), got \"{}\"").format(text)) return except IndexError: s = 0 self.set_time(None, GLib.Variant.new_int64(m * 60 + s)) def set_time(self, gaction, param): """ Set the talk time. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): The time in seconds, as an int64 variant """ time = param.get_int64() self.est_time = time self.label_ett.set_text("{:02}:{:02}".format(*divmod(self.est_time, 60))) def on_keypress(self, widget, event): """ Pass on keystrokes to :func:`~Gtk.Entry.do_key_press_event()`. """ return Gtk.Entry.do_key_press_event(self.entry_ett, event) def swap_label_for_entry(self, *args): """ Perform the actual work of starting the editing. """ Gio.Application.get_default().activate_action('cancel-page-number') # Set entry text self.entry_ett.set_text("{:02}:{:02}".format(*divmod(self.est_time, 60))) self.entry_ett.select_region(0, -1) # Replace label with entry self.eb_ett.remove(self.label_ett) self.eb_ett.add(self.entry_ett) self.entry_ett.show() self.entry_ett.grab_focus() self.editing = True def restore_label(self, gaction=None, param=None): """ Make sure that the current page number is displayed in a label and not in an entry. If it is an entry, then replace it with the label. """ child = self.eb_ett.get_child() if child is not self.label_ett: self.eb_ett.remove(child) self.eb_ett.add(self.label_ett) self.editing = False pympress-1.7.1/pympress/extras.py000066400000000000000000000706351415371354200171640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # extras.py # # Copyright 2017 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.extras` -- Manages the display of fancy extras such as annotations, videos and cursors ----------------------------------------------------------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import pathlib import mimetypes import functools from urllib.request import url2pathname import gi import cairo gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GLib, Gio from pympress import document, builder, util class Annotations(object): """ Widget displaying a PDF’s text annotations. """ #: The containing :class:`~Gtk.TreeView` widget for the annotations annotations_treeview = None #: The containing :class:`~Gtk.ListStore` storing the annotations to be displayed annotations_liststore = None #: The :class:`~Gtk.Entry` in which we are currently editing an annotation, or None editing = None new_doc_annotation = lambda *args: None set_doc_annotation = lambda *args: None remove_doc_annotation = lambda *args: None def __init__(self, builder): super(Annotations, self).__init__() builder.load_widgets(self) builder.setup_actions({ 'add-annotation': dict(activate=self.add_annotation), 'remove-annotation': dict(activate=self.remove_annotation), }) def load_annotations(self, annot_page): """ Add annotations to be displayed (typically on going to a new slide). Args: annot_page (:class:`~pympress.document.Page`): The page object that contains the annotations """ self.annotations_liststore.clear() for num, annot in enumerate(annot_page.get_annotations()): self.annotations_liststore.append([annot.get_contents(), num]) self.new_doc_annotation = annot_page.new_annotation self.set_doc_annotation = annot_page.set_annotation self.remove_doc_annotation = annot_page.remove_annotation def try_cancel(self): """ Try to cancel editing Returns: `bool`: whether editing was enabled and thus canceled """ if self.editing is None: return False rows = self.annotations_treeview.get_selection().get_selected_rows()[1] if rows: self.annotations_treeview.set_cursor(rows[0], None, False) return True def key_event(self, widget, event): """ Handle a key (press/release) event. Needed to forward events directly to the :class:`~Gtk.Entry`, bypassing the global action accelerators. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. Returns: `bool`: whether the event was consumed """ if self.editing is None: return False elif event.get_event_type() == Gdk.EventType.KEY_PRESS: return self.editing.do_key_press_event(self.editing, event) elif event.get_event_type() == Gdk.EventType.KEY_RELEASE: return self.editing.do_key_release_event(self.editing, event) return False def editing_started(self, cell_renderer, widget, entry_number): """ Handle edit start Args: cell_renderer (:class:`~Gtk.CellRenderer`): The renderer which received the signal widget (:class:`~Gtk.CellEditable`): the Gtk entry editing the annotation entry entry_number (`str`): the string representation of the path identifying the edited cell """ self.editing = widget def editing_validated(self, cell_renderer, entry_number, new_content): """ Handle successful edit: store the new cell value in the model and the document Args: cell_renderer (:class:`~Gtk.CellRenderer`): The renderer which received the signal entry_number (`str`): the string representation of the path identifying the edited cell new_content (`str`): the new value of the edited cell """ path = Gtk.TreePath.new_from_string(entry_number) row = self.annotations_liststore.get_iter(path) self.annotations_liststore.set_value(row, 0, new_content) self.set_doc_annotation(path.get_indices()[0], new_content) self.editing_finished(cell_renderer) def editing_finished(self, cell_renderer): """ Handle the end of editing Args: cell_renderer (:class:`~Gtk.CellRenderer`): The renderer which received the signal """ self.editing = None def add_annotation(self, gaction, param=None): """ Add an annotation to the the page’s annotation list Args: gaction (:class:`~Gio.Action`): the action triggering the call, which identifies which backend param (:class:`~GLib.Variant`): an optional parameter """ path = self.annotations_liststore.get_path(self.annotations_liststore.append()) self.annotations_treeview.set_cursor(path, self.annotations_treeview.get_columns()[0], True) self.new_doc_annotation(path.get_indices()[0]) return True def remove_annotation(self, gaction, param=None): """ Remove an annotation to the from the page’s annotation list Args: gaction (:class:`~Gio.Action`): the action triggering the call, which identifies which backend param (:class:`~GLib.Variant`): an optional parameter """ rows = self.annotations_treeview.get_selection().get_selected_rows()[1] if not rows: return False self.annotations_liststore.remove(self.annotations_liststore.get_iter(rows[0])) self.remove_doc_annotation(rows[0].get_indices()[0]) return True class Media(object): """ Class managing statically the medias and media player backends, to enable play/pause callbacks. Args: builder (:class:`~pympress.builder.Builder`): A builder from which to load widgets conf (:class:`~pympress.config.Config`): An object containing the preferences """ #: `dict` of :class:`~pympress.media_overlays.base.VideoOverlay` ready to be added on top of the slides _media_overlays = {} #: :class:`~Gtk.Overlay` for the Content window. c_overlay = None #: :class:`~Gtk.Overlay` for the Presenter window. p_overlay = None #: `dict` with the backend modules that were correctly loaded, mapping backend identifiers (`str`) #: to :class:`~pympress.media_overlays.base.VideoOverlay` sub-classes _backends = {} #: `dict` containing backends and their mappings to mime type lists for which they are enabled. #: A default backend is marked by an empty list. types_list = {} def __init__(self, builder, conf): super(Media, self).__init__() self.conf = conf self._setup_backends() builder.load_widgets(self) builder.setup_actions({ 'use-{}-backend'.format(backend): { 'activate': self.toggle, 'state': backend in self.types_list, 'enabled': backend in self._backends, } for backend in self._backends }) self.c_overlay.queue_draw() self.p_overlay.queue_draw() def toggle(self, gaction, param=None): """ Toggle a backend (if it was loaded correctly) Args: gaction (:class:`~Gio.Action`): the action triggering the call, which identifies which backend param (:class:`~GLib.Variant`): an optional parameter """ backend = gaction.get_name().split('-')[1] if backend not in self._backends: return ValueError('Unexpected backend') enable = backend not in self.types_list if enable: self.types_list[backend] = self.conf.getlist(backend, 'mime_types') else: del self.types_list[backend] gaction.change_state(GLib.Variant.new_boolean(enable)) self.conf.set(backend, 'enabled', 'on' if enable else 'off') def remove_media_overlays(self): """ Remove current media overlays. """ for media_id in self._media_overlays: self.hide(media_id) def purge_media_overlays(self): """ Remove current media overlays. """ self.remove_media_overlays() self._media_overlays.clear() def replace_media_overlays(self, current_page, page_type): """ Remove current media overlays, add new ones if page contains media. Args: current_page (:class:`~pympress.document.Page`): The page for which to prepare medias page_type (:class:`~pympress.document.PdfPage`): The part of the page to consider """ if page_type == document.PdfPage.NONE: return self.remove_media_overlays() for media in current_page.get_media(): media_id = hash(media) if media_id not in self._media_overlays: mime_type, enc = mimetypes.guess_type(media.filename) factory = self.get_factory(mime_type) if not factory: logger.warning('No available overlay for mime type {}, ignoring media {}' .format(mime_type, media.filename)) continue action_group = Gio.SimpleActionGroup.new() builder.Builder.setup_actions({ 'play': dict(activate=functools.partial(self.play, media_id)), 'stop': dict(activate=functools.partial(self.hide, media_id)), 'pause': dict(activate=functools.partial(self.play_pause, media_id)), 'set_time': dict(activate=functools.partial(self.set_time, media_id), parameter_type=float) }, action_group) v_da_c = factory(self.c_overlay, page_type, action_group, media) v_da_p = factory(self.p_overlay, page_type, action_group, media._replace(show_controls=True)) self._media_overlays[media_id] = (v_da_c, v_da_p) self._media_overlays[media_id][0].mute(True) self._media_overlays[media_id][1].mute(False) if any(overlay.autoplay for overlay in self._media_overlays[media_id]): GLib.idle_add(self.play, media_id) # TODO: handle poster def resize(self, which=None): """ Resize all media overlays that are a child of an overlay. """ needs_resizing = (which == 'content', which == 'presenter') if which is not None else (True, True) for media_id in self._media_overlays: for widget in (w for w, r in zip(self._media_overlays[media_id], needs_resizing) if r and w.is_shown()): widget.resize() def adjust_margins_for_mode(self, page_type): """ Adjust the relative margins of child widgets for notes mode update. Args: page_type (:class:`~pympress.document.PdfPage`): The part of the page to display """ for media_id in self._media_overlays: for widget in self._media_overlays[media_id]: widget.update_margins_for_page(page_type) def play(self, media_id, gaction=None, param=None): """ Starts playing a media. Used as a callback. Args: media_id (`int`): A unique identifier of the media to start playing gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ if media_id in self._media_overlays: c, p = self._media_overlays[media_id] p.show() c.show() GLib.idle_add(lambda: any(p.do_play() for p in self._media_overlays[media_id])) def hide(self, media_id, gaction=None, param=None): """ Stops playing a media and hides the player. Used as a callback. Args: media_id (`int`): A unique identifier of the media to start playing gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ if media_id in self._media_overlays: c, p = self._media_overlays[media_id] if c.is_shown(): c.do_hide() if p.is_shown(): p.do_hide() def hide_all(self): """ Stops all playing medias and hides the players. Used before exit. """ for c, p in self._media_overlays.values(): if c.is_shown(): c.do_hide() if p.is_shown(): p.do_hide() def play_pause(self, media_id, gaction=None, param=None): """ Toggles playing and pausing a media. Used as a callback. Args: media_id (`int`): A unique idientifier of the media to start playing gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ GLib.idle_add(lambda: any(p.do_play_pause() for p in self._media_overlays[media_id])) def set_time(self, media_id, gaction=None, param=None): """ Set the player of a given media at time t. Used as a callback. Args: media_id (`int`): A unique idientifier of the media to start playing gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): A wrapped float containing the time to which we have to go. """ t = param.get_double() GLib.idle_add(lambda: any(p.do_set_time(t) for p in self._media_overlays[media_id])) def _setup_backends(self): """ Load the backends for video overlays. """ try: from pympress.media_overlays.gif_backend import GifOverlay gif_version = GifOverlay.setup_backend() self._backends['gif'] = GifOverlay self.types_list['gif'] = ['image/gif', 'image/svg+xml'] except Exception as e: gif_version = 'GdkPixbuf not available' logger.error(_('Media support using {} is disabled.').format('GdkPixbuf')) logger.info(_('Caused by ') + type(e).__name__ + ': ' + str(e)) if not self.conf.getboolean('gstreamer', 'enabled'): gst_version = 'GStreamer disabled' else: try: from pympress.media_overlays.gst_backend import GstOverlay gst_version = GstOverlay.setup_backend(self.conf.getlist('gstreamer', 'init_options')) except Exception as e: gst_version = 'GStreamer not available' logger.debug(_('Media support using {} is disabled.').format('GStreamer')) logger.debug(_('Caused by ') + type(e).__name__ + ': ' + str(e)) else: self._backends['gstreamer'] = GstOverlay self.types_list['gstreamer'] = self.conf.getlist('gstreamer', 'mime_types') if not self.conf.getboolean('vlc', 'enabled'): vlc_version = 'VLC disabled' else: try: from pympress.media_overlays.vlc_backend import VlcOverlay vlc_version = VlcOverlay.setup_backend(self.conf.getlist('vlc', 'init_options')) except Exception as e: vlc_version = 'VLC not available' logger.debug(_('Media support using {} is disabled.').format('VLC')) logger.debug(_('Caused by ') + type(e).__name__ + ': ' + str(e)) else: self._backends['vlc'] = VlcOverlay self.types_list['vlc'] = self.conf.getlist('vlc', 'mime_types') self.backend_version = ', '.join([gif_version, gst_version, vlc_version]) logger.info(_('Media support: ') + self.backend_version) def get_factory(self, mime_type): """ Returns a class of type :attr:`~_backend`. """ # Search for specific mime type, if no matches fall back to empty lists which mean any mime types options = [backend for backend, mime_types in self.types_list.items() if mime_type in mime_types] if not options: options = [backend for backend, mime_types in self.types_list.items() if len(mime_types) == 0] if not options: return None # Prefer more stable backends with less external dependencies priority = ['gif', 'vlc', 'gstreamer'] if util.IS_WINDOWS else ['gif', 'gstreamer', 'vlc'] return self._backends[sorted(options, key=priority.index)[0]] class Cursor(object): """ Class managing cursors statically for displays, so we can select the mouse cursor with a simple string. """ #: a static `dict` of :class:`~Gdk.Cursor`s, ready to use _cursors = { 'parent': None, } @classmethod def _populate_cursors(cls): cls._cursors.update({ 'default': Gdk.Cursor.new_for_display(Gdk.Display.get_default(), Gdk.CursorType.LEFT_PTR), 'pointer': Gdk.Cursor.new_for_display(Gdk.Display.get_default(), Gdk.CursorType.HAND1), 'crosshair': Gdk.Cursor.new_for_display(Gdk.Display.get_default(), Gdk.CursorType.CROSSHAIR), 'invisible': Gdk.Cursor.new_for_display(Gdk.Display.get_default(), Gdk.CursorType.BLANK_CURSOR), }) @classmethod def set_cursor(cls, widget, cursor_name = 'parent'): """ Set the cursor named cursor_name'. Args: widget (:class:`~Gtk.Widget`): The widget triggering the cursor change, used to retrieve a Gdk.Window cursor_name (`str`): Name of the cursor to be set """ try: cursor = cls._cursors[cursor_name] except KeyError: cls._populate_cursors() cursor = cls._cursors[cursor_name] window = widget.get_window() if window is not None: window.set_cursor(cursor) class Zoom(object): """ Manage the zoom level (using a cairo matrix), draw area that will be zoomed while it is being selected. Args: builder (:class:`~pympress.builder.Builder`): A builder from which to load widgets """ #: Whether we are displaying the interface to scribble on screen and the overlays containing said scribbles zoom_selecting = False zoom_points = None scale = 1. shift = (0, 0) #: :class:`~Gtk.Box` in the Presenter window, used to reliably set cursors. p_central = None #: callback, to be connected to :meth:`~pympress.app.Pympress.set_action_enabled` set_action_enabled = None #: callback, to be connected to :func:`~pympress.ui.UI.redraw_current_slide` redraw_current_slide = lambda *args: None #: callback, to be connected to :func:`~pympress.ui.UI.clear_cache` clear_cache = lambda *args: None def __init__(self, builder): super(Zoom, self).__init__() builder.load_widgets(self) self.redraw_current_slide = builder.get_callback_handler('redraw_current_slide') self.clear_cache = builder.get_callback_handler('clear_zoom_cache') self.set_action_enabled = builder.get_callback_handler('app.set_action_enabled') builder.setup_actions({ 'zoom': dict(activate=self.start_zooming), 'unzoom': dict(activate=self.stop_zooming), }) def start_zooming(self, *args): """ Setup for the user to select the zooming area. Returns: `bool`: whether the event was consumed """ self.zoom_selecting = True Cursor.set_cursor(self.p_central, 'crosshair') return True def stop_zooming(self, *args): """ Cancel the zooming, reset the zoom level to full page. Returns: `bool`: whether the event was consumed """ Cursor.set_cursor(self.p_central) self.zoom_selecting = False self.zoom_points = None self.scale = 1. self.shift = (0, 0) self.set_action_enabled('unzoom', False) self.redraw_current_slide() self.clear_cache() return True def try_cancel(self): """ Cancel the zoom selection, if it was enabled. Returns: `bool`: `True` if the zoom was cancelled, `False` if a zoom selection was not in progress. """ if not self.zoom_selecting: return False Cursor.set_cursor(self.p_central) self.zoom_selecting = False self.zoom_points = None return True def get_slide_point(self, widget, event): """ Gets the point on the slide on a scale (0..1, 0..1), from its position in the widget. """ ww, wh = widget.get_allocated_width(), widget.get_allocated_height() ex, ey = event.get_coords() return ((ex / ww - self.shift[0]) / self.scale, (ey / wh - self.shift[1]) / self.scale) def get_matrix(self, ww, wh): """ Returns the :class:`~cairo.Matrix` used to perform the zoom for the widget of size ww x wh. Args: ww (`float`): widget width wh (`float`): widget height Returns: :class:`~cairo.Matrix`: the zoom transformation matrix """ return cairo.Matrix(xx = self.scale, x0 = ww * self.shift[0], yy = self.scale, y0 = wh * self.shift[1]) def track_zoom_target(self, widget, event): """ Draw the zoom's target rectangle. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. Returns: `bool`: whether the event was consumed """ if self.zoom_selecting and self.zoom_points: self.zoom_points[1] = self.get_slide_point(widget, event) self.redraw_current_slide() return True return False def toggle_zoom_target(self, widget, event): """ Start/stop drawing the zoom's target rectangle. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. Returns: `bool`: whether the event was consumed """ if not self.zoom_selecting: return False if event.get_event_type() == Gdk.EventType.BUTTON_PRESS: p = self.get_slide_point(widget, event) self.zoom_points = [p, p] return self.track_zoom_target(widget, event) elif event.get_event_type() == Gdk.EventType.BUTTON_RELEASE and self.zoom_points: self.zoom_points[1] = self.get_slide_point(widget, event) xmin, xmax = sorted(p[0] for p in self.zoom_points) ymin, ymax = sorted(p[1] for p in self.zoom_points) self.zoom_points = None try: # zoom by dimension less zoomed, to fit box while maintaining aspect ratio self.scale = 1. / max(ymax - ymin, xmax - xmin) # make center of drawn rectangle the center of the zoomed slide self.shift = (.5 - self.scale * (xmin + xmax) / 2, .5 - self.scale * (ymin + ymax) / 2) except ZeroDivisionError: self.scale = 1. self.shift = (0, 0) # stop drawing rectangles and reset cursor (NB don't use window, this bugs) Cursor.set_cursor(self.p_central) self.zoom_selecting = False self.clear_cache() self.redraw_current_slide() self.set_action_enabled('unzoom', True) return True return False def draw_zoom_target(self, widget, cairo_context): """ Perform the drawings by user. Args: widget (:class:`~Gtk.DrawingArea`): The widget where to draw the scribbles. cairo_context (:class:`~cairo.Context`): The canvas on which to render the drawings """ ww, wh = widget.get_allocated_width(), widget.get_allocated_height() if self.zoom_selecting and self.zoom_points: xmin, xmax = sorted(p[0] * ww for p in self.zoom_points) ymin, ymax = sorted(p[1] * wh for p in self.zoom_points) rect = Gdk.Rectangle() rect.x = xmin rect.width = xmax - xmin rect.y = ymin rect.height = ymax - ymin cairo_context.set_line_width(3) cairo_context.set_line_cap(cairo.LINE_CAP_SQUARE) Gdk.cairo_rectangle(cairo_context, rect) cairo_context.set_source_rgba(.1, .1, 1, .4) cairo_context.stroke() Gdk.cairo_rectangle(cairo_context, rect) cairo_context.set_source_rgba(.5, .5, 1, .2) cairo_context.fill() def nop(*args, **kwargs): """ Do nothing """ pass class FileWatcher(object): """ A class that wraps watchdog objects, to trigger callbacks when a file changes. """ #: A :class:`~watchdog.observers.Observer` to watch when the file changes observer = None #: A :class:`~watchdog.events.FileSystemEventHandler` to get notified when the file changes monitor = None # `int` that is a GLib timeout id to delay the callback timeout = 0 #: The :class:`~pathlib.Path` to the file being watched path = None #: Callback to be called on file changes, usually connected to :meth:`~pympress.ui.UI.reload_document` callback = lambda: None def __init__(self): try: from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler self.observer = Observer() self.monitor = FileSystemEventHandler() self.observer.start() except ImportError: logger.error(_('Missing dependency: python "{}" package').format('watchdog')) logger.info(_('Monitoring of changes to reload files automatically is not available')) def __del__(self): """ On finalize, cancel the watchdog observer thread. """ self.stop_watching() if self.observer.is_alive(): self.observer.stop() self.observer = None def watch_file(self, uri, callback, *args, **kwargs): """ Watches a new file with a new callback. Removes any precedent watched files. If the optional watchdog dependency is missing, does nothing. Args: uri (`str`): URI of the file to watch callback (`function`): callback to call with all the further arguments when the file changes """ if self.observer is None: return self.stop_watching() scheme, path = uri.split('://', 1) self.path = pathlib.Path(url2pathname(path)) if scheme != 'file': logger.error('Impossible to watch files with {} schemes'.format(scheme), exc_info = True) return self.callback = lambda: callback(*args, **kwargs) self.monitor.on_modified = self._enqueue try: self.observer.schedule(self.monitor, str(self.path.parent), recursive=False) except OSError: logger.error('Impossible to open dir at {}'.format(str(self.path.parent)), exc_info = True) def stop_watching(self): """ Remove all files that are being watched. """ self.observer.unschedule_all() def _enqueue(self, event): """ Call callback with delay, to avoid repeated calls in short periods of time. Args: event (:class:`~watchdog.events.FileSystemEvent`): the event that caused the callback to be triggered """ if event.src_path != str(self.path): return if self.timeout: GLib.Source.remove(self.timeout) self.timeout = GLib.timeout_add(200, self._call) def _call(self): """ Call the callback. """ if self.timeout: self.timeout = 0 self.callback() pympress-1.7.1/pympress/media_overlays/000077500000000000000000000000001415371354200202745ustar00rootroot00000000000000pympress-1.7.1/pympress/media_overlays/__init__.py000066400000000000000000000017471415371354200224160ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # media_overlays/__init__.py # # Copyright 2018 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ Backends for video overlay widgets. """ __all__ = ['base', 'gif_backend', 'gst_backend', 'vlc_backend'] pympress-1.7.1/pympress/media_overlays/base.py000066400000000000000000000241631415371354200215660ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # media_overlays/base.py # # Copyright 2015 Cimbali # # Vaguely inspired from: # gtk example/widget for VLC Python bindings # Copyright (C) 2009-2010 the VideoLAN team # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. # """ :mod:`pympress.media_overlays.base` -- widget to play videos with a backend like VLC ------------------------------------------------------------------------------------ """ import logging logger = logging.getLogger(__name__) import gi gi.require_version('Gtk', '3.0') from gi.repository import GLib from pympress import builder class VideoOverlay(builder.Builder): """ Simple Video widget. Args: container (:class:`~Gtk.Overlay`): The container with the slide, at the top of which we add the movie area page_type (:class:`~pympress.document.PdfPage`): the part of the page to display action_map (:class:`~Gio.ActionMap`): the action map that contains the actions for this media media (:class:`~pympress.document.Media`): the object defining the properties of the video such as position etc. """ #: :class:`~Gtk.Overlay` that is the parent of the VideoOverlay widget. parent = None #: :class:`~Gtk.VBox` that contains all the elements to be overlayed. media_overlay = None #: A :class:`~Gtk.HBox` containing a toolbar with buttons and :attr:`~progress` the progress bar toolbar = None #: :class:`~Gtk.Scale` that is the progress bar in the controls toolbar - if we have one. progress = None #: :class:`~Gtk.DrawingArea` where the media is rendered. movie_zone = None #: `tuple` containing the left/top/right/bottom space around the drawing area in the PDF page relative_page_margins = None #: `tuple` containing the left/top/right/bottom space around the drawing area in the visible slide relative_margins = None #: `bool` that tracks whether we should play automatically autoplay = False #: `bool` that tracks whether we should play after we finished playing repeat = False #: `bool` that tracks whether the user is dragging the position dragging_position = False #: `bool` that tracks whether the playback was paused when the user started dragging the position dragging_paused = False #: Format of the video time, defaults to m:ss, changed to m:ss / m:ss when the max time is known time_format = '{:01}:{:02}' #: `float` holding the max time in s maxval = 1 #: :class:`~Gio.ActionMap` containing the actios for this video overlay action_map = None def __init__(self, container, page_type, action_map, media): super(VideoOverlay, self).__init__() self.parent = container self.relative_page_margins = tuple(getattr(media.relative_margins, v) for v in ('x1', 'y2', 'x2', 'y1')) self.update_margins_for_page(page_type) self.load_ui('media_overlay') self.toolbar.set_visible(media.show_controls) self.connect_signals(self) # medias, here the actions are scoped to the current widget self.action_map = action_map self.media_overlay.insert_action_group('media', self.action_map) self.set_file(media.filename) self.autoplay = media.autoplay self.repeat = media.repeat # TODO: handle poster def handle_embed(self, mapped_widget): """ Handler to embed the video player in the window, connected to the :attr:`~.Gtk.Widget.signals.map` signal. """ return False def format_millis(self, sc, prog): """ Callback to format the current timestamp (in milliseconds) as minutes:seconds. Args: sc (:class:`~Gtk.Scale`): The scale whose position we are formatting prog (`float`): The position of the :class:`~Gtk.Scale`, i.e. the number of seconds elapsed """ return self.time_format.format(*divmod(int(round(prog)), 60)) def update_range(self, max_time): """ Update the toolbar slider size. Args: max_time (`float`): The maximum time in this video in s """ self.maxval = max_time self.progress.set_range(0, self.maxval) self.progress.set_increments(min(5., self.maxval / 10.), min(60., self.maxval / 10.)) sec = round(self.maxval) if self.maxval > .5 else 1. self.time_format = '{{:01}}:{{:02}} / {:01}:{:02}'.format(*divmod(int(sec), 60)) def update_progress(self, time): """ Update the toolbar slider to the current time. Args: time (`float`): The time in this video in s """ self.progress.set_value(time) def progress_moved(self, rng, sc, val): """ Callback to update the position of the video when the user moved the progress bar. Args: rng (:class:`~Gtk.Range`): The range corresponding to the scale whose position we are formatting sc (:class:`~Gtk.Scale`): The scale whose position we are updating val (`float`): The position of the :class:`~Gtk.Scale`, which is the number of seconds elapsed in the video """ return self.action_map.lookup_action('set_time').activate(GLib.Variant.new_double(val)) def play_pause(self, *args): """ Callback to toggle play/pausing from clicking on the DrawingArea """ return self.action_map.lookup_action('pause').activate() def handle_end(self): """ End of the stream reached: restart if looping, otherwise hide overlay """ if not self.repeat: self.action_map.lookup_action('stop').activate() else: self.action_map.lookup_action('set_time').activate(GLib.Variant.new_double(0)) def update_margins_for_page(self, page_type): """ Recalculate the margins around the media in the event of a page type change. Arguments: page_type (:class:`~pympress.document.PdfPage`): the part of the page to display """ self.relative_margins = page_type.to_screen(*self.relative_page_margins) def resize(self): """ Adjust the position and size of the media overlay. """ if not self.is_shown(): return pw, ph = self.parent.get_allocated_width(), self.parent.get_allocated_height() self.media_overlay.props.margin_left = pw * max(self.relative_margins[0], 0) self.media_overlay.props.margin_right = pw * max(self.relative_margins[2], 0) self.media_overlay.props.margin_bottom = ph * max(self.relative_margins[3], 0) self.media_overlay.props.margin_top = ph * max(self.relative_margins[1], 0) def is_shown(self): """ Returns whether the media overlay is currently added to the overlays, or hidden. Returns: `bool`: `True` iff the overlay is currently displayed. """ return self.media_overlay.get_parent() is not None def is_playing(self): """ Returns whether the media is currently playing (and not paused). Returns: `bool`: `True` iff the media is playing. """ raise NotImplementedError def do_stop(self): """ Stops playing in the backend player. """ raise NotImplementedError def set_file(self, filepath): """ Sets the media file to be played by the widget. Args: filepath (`pathlib.Path`): The path to the media file path """ raise NotImplementedError def show(self): """ Bring the widget to the top of the overlays if necessary. """ if min(self.relative_margins) < 0: logger.warning('Negative margin(s) clipped to 0 (might alter the aspect ratio?): ' + 'LTRB = {}'.format(self.relative_margins)) if not self.media_overlay.get_parent(): self.parent.add_overlay(self.media_overlay) self.parent.reorder_overlay(self.media_overlay, 2) self.resize() self.parent.queue_draw() self.media_overlay.show() def do_hide(self, *args): """ Remove widget from overlays. Needs to be called via :func:`~GLib.idle_add`. Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ self.do_stop() self.media_overlay.hide() if self.media_overlay.get_parent(): self.parent.remove(self.media_overlay) self.parent.queue_draw() return False def do_play(self): """ Start playing the media file. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Returns: `bool`: `True` iff this function should be run again (:meth:`~GLib.idle_add` convention) """ raise NotImplementedError def do_play_pause(self): """ Toggle pause mode of the media. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Returns: `bool`: `True` iff this function should be run again (:meth:`~GLib.idle_add` convention) """ raise NotImplementedError def do_set_time(self, t): """ Set the player at time t. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Args: t (`float`): the timestamp, in s Returns: `bool`: `True` iff this function should be run again (:meth:`~GLib.idle_add` convention) """ raise NotImplementedError pympress-1.7.1/pympress/media_overlays/gif_backend.py000066400000000000000000000115461415371354200230710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # media_overlays/gif.py # # Copyright 2018 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. # """ :mod:`pympress.media_overlays.gif` -- widget to play gif images as videos ------------------------------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import gi import cairo gi.require_version('Gtk', '3.0') from gi.repository import Gdk, GLib, GdkPixbuf from pympress.media_overlays import base class GifOverlay(base.VideoOverlay): """ A simple overlay mimicking the functionality of showing videos, but showing gifs instead. """ #: A :class:`~GdkPixbuf.PixbufAnimation` containing all the frames and their timing for the displayed gif anim = None #: A :class:`~GdkPixbuf.PixbufAnimationIter` which will provide the timely access to the frames in `~anim` iter = None #: A `tuple` of (`int`, `int`) indicating the size of the bounding box of the gif base_size = None #: The :class:`~cairo.Matrix` defining the zoom & shift to scale the gif transform = None def __init__(self, *args, **kwargs): super(GifOverlay, self).__init__(*args, **kwargs) # override: no toolbar or interactive stuff for a gif, replace the whole widget area with a GdkPixbuf self.autoplay = True self.toolbar.set_visible(False) # we'll manually draw on the movie zone self.movie_zone.connect('draw', self.draw) self.movie_zone.connect('configure-event', self.set_transform) def set_file(self, filepath): """ Sets the media file to be played by the widget. Args: filepath (`pathlib.Path`): The path to the media file path """ self.anim = GdkPixbuf.PixbufAnimation.new_from_file(str(filepath)) self.base_size = (self.anim.get_width(), self.anim.get_height()) self.iter = self.anim.get_iter(None) self.set_transform() self.advance_gif() def set_transform(self, *args): """ Compute the transform to scale (not stretch nor crop) the gif. """ widget_size = (self.movie_zone.get_allocated_width(), self.movie_zone.get_allocated_height()) scale = min(widget_size[0] / self.base_size[0], widget_size[1] / self.base_size[1]) dx = widget_size[0] - scale * self.base_size[0] dy = widget_size[1] - scale * self.base_size[1] self.transform = cairo.Matrix(xx = scale, yy = scale, x0 = dx / 2, y0 = dy / 2) def draw(self, widget, ctx): """ Simple resized drawing: get the pixbuf, set the transform, draw the image. """ if self.iter is None: return False try: ctx.transform(self.transform) Gdk.cairo_set_source_pixbuf(ctx, self.iter.get_pixbuf(), 0, 0) ctx.paint() except cairo.Error: logger.error(_('Cairo can not draw gif'), exc_info = True) def advance_gif(self): """ Advance the gif, queue redrawing if the frame changed, and schedule the next frame. """ if self.iter.advance(): self.movie_zone.queue_draw() delay = self.iter.get_delay_time() if delay >= 0: GLib.timeout_add(delay, self.advance_gif) def do_set_time(self, t): """ Set the player at time t. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Args: t (`int`): the timestamp, in ms Returns: `bool`: `True` iff this function should be run again (:meth:`~GLib.idle_add` convention) """ start = GLib.TimeVal() GLib.DateTime.new_now_local().to_timeval(start) start.add(-t) self.iter = self.anim.get_iter(start) self.advance_gif() return False # a bunch of inherited functions that do nothing, for gifs def mute(self, *args): pass def is_playing(self): return True def do_stop(self): pass def do_play(self): return False def do_play_pause(self): return False @classmethod def setup_backend(cls): """ Returns the name of this backend. """ return _('GdkPixbuf gif player') pympress-1.7.1/pympress/media_overlays/gst_backend.py000066400000000000000000000132401415371354200231120ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # media_overlays/gst.py # # Copyright 2018 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. # """ :mod:`pympress.media_overlays.gst` -- widget to play videos using Gstreamer's Gst --------------------------------------------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import gi gi.require_version('Gtk', '3.0') gi.require_version('Gst', '1.0') from gi.repository import GLib, Gst from pympress.media_overlays import base class GstOverlay(base.VideoOverlay): """ Simple Gstramer widget. Wraps a simple gstreamer playbin. """ #: A :class:`~Gst.Playbin` to be play videos playbin = None def __init__(self, *args, **kwargs): # Create GStreamer playbin self.playbin = Gst.ElementFactory.make('playbin', None) self.sink = Gst.ElementFactory.make('gtksink', None) self.playbin.set_property('video-sink', self.sink) super(GstOverlay, self).__init__(*args, **kwargs) self.media_overlay.remove(self.movie_zone) self.media_overlay.pack_start(self.sink.props.widget, True, True, 0) self.media_overlay.reorder_child(self.sink.props.widget, 0) self.sink.props.widget.hide() # Create bus to get events from GStreamer playin bus = self.playbin.get_bus() bus.add_signal_watch() bus.enable_sync_message_emission() bus.connect('message::eos', lambda *args: GLib.idle_add(self.handle_end)) bus.connect('message::error', lambda _, msg: logger.error('{} {}'.format(*msg.parse_error()))) bus.connect('message::async-done', self.on_play) bus.connect('message::duration-changed', lambda *args: GLib.idle_add(self.do_update_duration)) def is_playing(self): """ Returns whether the media is currently playing (and not paused). Returns: `bool`: `True` iff the media is playing. """ return self.playbin.get_state(0).state == Gst.State.PLAYING def set_file(self, filepath): """ Sets the media file to be played by the widget. Args: filepath (`pathlib.Path`): The path to the media file path """ self.playbin.set_property('uri', filepath.as_uri()) self.playbin.set_state(Gst.State.READY) def mute(self, value): """ Mutes or unmutes the player. Args: value (`bool`): `True` iff this player should be muted """ self.playbin.set_property('mute', value) return False def on_play(self, *args): """ Start the scroll bar updating process. """ GLib.idle_add(self.do_update_duration) GLib.timeout_add(200, self.do_update_time) self.sink.props.widget.show() def do_update_duration(self, *args): """ Transmit the change of file duration to the UI to adjust the scroll bar. """ changed, time_ns = self.playbin.query_duration(Gst.Format.TIME) self.update_range(max(0, time_ns) / 1e9) def do_update_time(self): """ Start playing the media file. Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ changed, time_ns = self.playbin.query_position(Gst.Format.TIME) self.update_progress(time_ns / 1e9) return True def do_play(self): """ Start playing the media file. Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ self.playbin.set_state(Gst.State.PLAYING) return False def do_play_pause(self): """ Toggle pause mode of the media. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ self.playbin.set_state(Gst.State.PLAYING if not self.is_playing() else Gst.State.PAUSED) return False def do_stop(self): """ Stops playing in the backend player. """ self.playbin.set_state(Gst.State.NULL) self.playbin.set_state(Gst.State.READY) self.sink.props.widget.hide() return False def do_set_time(self, t): """ Set the player at time t. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Args: t (`float`): the timestamp, in s Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ self.playbin.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, t * Gst.SECOND) return False @classmethod def setup_backend(cls, gst_opts = []): """ Prepare/check the Gst backend. Returns: `str`: the version of Gst used by the backend """ Gst.init(gst_opts) return Gst.version_string() pympress-1.7.1/pympress/media_overlays/vlc_backend.py000066400000000000000000000171161415371354200231070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # media_overlays/vlc.py # # Copyright 2018 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. # """ :mod:`pympress.media_overlays.vlc` -- widget to play videos using VLC --------------------------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import os import vlc import ctypes import gi gi.require_version('Gtk', '3.0') from gi.repository import GLib from pympress.util import IS_WINDOWS from pympress.media_overlays import base def get_window_handle(window): """ Uses ctypes to call gdk_win32_window_get_handle which is not available in python gobject introspection porting. Solution from http://stackoverflow.com/a/27236258/1387346 Args: window (:class:`~Gdk.Window`): The window for which we want to get the handle Returns: The handle to the win32 window """ # get the c gpointer of the gdk window ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object] drawingarea_gpointer = ctypes.pythonapi.PyCapsule_GetPointer(window.__gpointer__, None) # get the win32 handle gdkdll = ctypes.CDLL('libgdk-3-0.dll') handle_getter = gdkdll.gdk_win32_window_get_handle handle_getter.restype = ctypes.c_void_p handle_getter.argtypes = [ctypes.c_void_p] return handle_getter(drawingarea_gpointer) class VlcOverlay(base.VideoOverlay): """ Simple VLC widget. Its player can be controlled through the 'player' attribute, which is a :class:`~vlc.MediaPlayer` instance. """ #: A single vlc.Instance() to be shared by (possible) multiple players. _instance = None def __init__(self, *args, **kwargs): self.player = self._instance.media_player_new() # before loading UI, needed to connect "map" signal super(VlcOverlay, self).__init__(*args, **kwargs) # Simple black background painting to avoid glitching outside of video area self.movie_zone.connect('draw', self.paint_backdrop) event_manager = self.player.event_manager() event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, lambda e: GLib.idle_add(self.handle_end)) event_manager.event_attach(vlc.EventType.MediaPlayerLengthChanged, lambda e: self.update_range(self.player.get_length() / 1000. or 1.)) event_manager.event_attach(vlc.EventType.MediaPlayerTimeChanged, lambda e: self.update_progress(self.player.get_time() / 1000. or 1.)) def handle_embed(self, mapped_widget): """ Handler to embed the VLC player in the window, connected to the :attr:`~.Gtk.Widget.signals.map` signal. """ # Do we need to be on the main thread? (especially for the mess from the win32 window handle) # assert(isinstance(threading.current_thread(), threading._MainThread)) window = self.movie_zone.get_window() if window is None: logger.error('No window in which to embed the VLC player!') return False elif IS_WINDOWS: self.player.set_hwnd(get_window_handle(window)) # get_property('window') else: self.player.set_xwindow(window.get_xid()) self.movie_zone.queue_draw() return False def is_playing(self): """ Returns whether the media is currently playing (and not paused). Returns: `bool`: `True` iff the media is playing. """ return self.player.is_playing() def set_file(self, filepath): """ Sets the media file to be played by the widget. Args: filepath (`pathlib.Path`): The path to the media file path """ self.player.set_media(self._instance.media_new(filepath.resolve().as_uri())) def handle_end(self): """ End of the stream reached: restart if looping, otherwise hide overlay """ self.action_map.lookup_action('stop').activate() if self.repeat: self.action_map.lookup_action('set_time').activate(GLib.Variant.new_double(0)) self.action_map.lookup_action('play').activate() def mute(self, value): """ Mutes the player. Args: value (`bool`): `True` iff this player should be muted """ GLib.idle_add(self.player.audio_set_volume, 0 if value else 100) return False def do_play(self): """ Start playing the media file. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ self.player.play() self.movie_zone.queue_draw() return False def paint_backdrop(self, widget, context): """ Draw behind/around the video, aka the black bars Args: widget (:class:`~Gtk.Widget`): the widget to update context (:class:`~cairo.Context`): the Cairo context (or `None` if called directly) """ context.save() context.set_source_rgb(0, 0, 0) context.fill() context.paint() context.restore() def show(self): """ Bring the widget to the top of the overlays if necessary − also force redraw of movie zone """ super(VlcOverlay, self).show() self.movie_zone.queue_draw() def do_play_pause(self): """ Toggle pause mode of the media. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ self.player.pause() if self.player.is_playing() else self.player.play() return False def do_stop(self): """ Stops playing in the backend player. """ self.player.stop() def do_set_time(self, t): """ Set the player at time t. Should run on the main thread to ensure we avoid vlc plugins' reentrency problems. Args: t (`float`): the timestamp, in s Returns: `bool`: `True` iff this function should be run again (:func:`~GLib.idle_add` convention) """ self.player.set_time(int(round(t * 1000.))) return False @classmethod def setup_backend(cls, vlc_opts = ['--no-video-title-show']): """ Prepare/check the VLC backend. Args: vlc_opts (`list`): the arguments for starting vlc Returns: `str`: the version of VLC used by the backend """ if IS_WINDOWS and vlc.plugin_path: # let python find the DLLs os.environ['PATH'] = vlc.plugin_path + ';' + os.environ['PATH'] VlcOverlay._instance = vlc.Instance(vlc_opts) return 'VLC {}'.format(vlc.libvlc_get_version().decode('ascii')) pympress-1.7.1/pympress/pointer.py000066400000000000000000000252451415371354200173330ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # pointer.py # # Copyright 2017 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.pointer` -- Manage when and where to draw a software-emulated laser pointer on screen ---------------------------------------------------------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import enum import gi gi.require_version('Gtk', '3.0') from gi.repository import Gdk, GdkPixbuf, GLib from pympress import util, extras class PointerMode(enum.Enum): """ Possible values for the pointer. """ #: Pointer switched on continuously CONTINUOUS = 2 #: Pointer switched on only manual MANUAL = 1 #: Pointer never switched on DISABLED = 0 class Pointer(object): """ Manage and draw the software “laser pointer” to point at the slide. Displays a pointer of chosen color on the current slide (in both windows), either on all the time or only when clicking while ctrl pressed. Args: config (:class:`~pympress.config.Config`): A config object containing preferences builder (:class:`~pympress.builder.Builder`): A builder from which to load widgets """ #: :class:`~GdkPixbuf.Pixbuf` to read XML descriptions of GUIs and load them. pointer = GdkPixbuf.Pixbuf() #: `(float, float)` of position relative to slide, where the pointer should appear pointer_pos = (.5, .5) #: `bool` indicating whether we should show the pointer show_pointer = False #: :class:`~pympress.pointer.PointerMode` indicating the pointer mode pointer_mode = PointerMode.MANUAL #: The :class:`~pympress.pointer.PointerMode` to which we toggle back old_pointer_mode = PointerMode.CONTINUOUS #: A reference to the UI's :class:`~pympress.config.Config`, to update the pointer preference config = None #: :class:`~Gtk.DrawingArea` Slide in the Presenter window, used to reliably set cursors. p_da_cur = None #: :class:`~Gtk.DrawingArea` Slide in the Contents window, used to reliably set cursors. c_da = None #: :class:`~Gtk.AspectFrame` Frame of the Contents window, used to reliably set cursors. c_frame = None #: a `dict` of the :class:`~Gtk.RadioMenuItem` selecting the pointer mode pointermode_radios = {} #: callback, to be connected to :func:`~pympress.ui.UI.redraw_current_slide` redraw_current_slide = lambda *args: None #: callback, to be connected to :meth:`~pympress.app.Pympress.set_action_state` set_action_state = None def __init__(self, config, builder): super(Pointer, self).__init__() self.config = config builder.load_widgets(self) self.redraw_current_slide = builder.get_callback_handler('redraw_current_slide') self.set_action_state = builder.get_callback_handler('app.set_action_state') default_mode = config.get('presenter', 'pointer_mode') default_color = config.get('presenter', 'pointer') try: default_mode = PointerMode[default_mode.upper()] except KeyError: default_mode = PointerMode.MANUAL self.activate_pointermode(default_mode) self.load_pointer(default_color) self.action_map = builder.setup_actions({ 'pointer-color': dict(activate=self.change_pointercolor, state=default_color, parameter_type=str), 'pointer-mode': dict(activate=self.change_pointermode, state=default_mode.name.lower(), parameter_type=str), }) def load_pointer(self, name): """ Perform the change of pointer using its color name. Args: name (`str`): Name of the pointer to load """ if name in ['red', 'green', 'blue']: self.pointer = GdkPixbuf.Pixbuf.new_from_file(util.get_icon_path('pointer_' + name + '.png')) else: raise ValueError('Wrong color name') def change_pointercolor(self, action, target): """ Callback for a radio item selection as pointer mode (continuous, manual, none). Args: action (:class:`~Gio.Action`): The action activatd target (:class:`~GLib.Variant`): The selected mode """ color = target.get_string() self.load_pointer(color) self.config.set('presenter', 'pointer', color) action.change_state(target) def activate_pointermode(self, mode=None): """ Activate the pointer as given by mode. Depending on the given mode, shows or hides the laser pointer and the normal mouse pointer. Args: mode (:class:`~pympress.pointer.PointerMode`): The mode to activate """ # Set internal variables, unless called without mode (from ui, after windows have been mapped) if mode == self.pointer_mode: return elif mode is not None: self.old_pointer_mode, self.pointer_mode = self.pointer_mode, mode self.config.set('presenter', 'pointer_mode', self.pointer_mode.name.lower()) # Set mouse pointer and cursors on/off, if windows are already mapped self.show_pointer = False for slide_widget in [self.p_da_cur, self.c_da]: ww, wh = slide_widget.get_allocated_width(), slide_widget.get_allocated_height() if max(ww, wh) == 1: continue window = slide_widget.get_window() pointer_coordinates = window.get_pointer() if window is not None else (-1, -1) if 0 < pointer_coordinates.x < ww and 0 < pointer_coordinates.y < wh \ and self.pointer_mode == PointerMode.CONTINUOUS: # Laser activated right away self.pointer_pos = (pointer_coordinates.x / ww, pointer_coordinates.y / wh) self.show_pointer = True extras.Cursor.set_cursor(slide_widget, 'invisible') else: extras.Cursor.set_cursor(slide_widget, 'parent') self.redraw_current_slide() def change_pointermode(self, action, target): """ Callback for a radio item selection as pointer mode (continuous, manual, none). Args: action (:class:`~Gio.Action`): The action activatd target (:class:`~GLib.Variant`): The selected mode """ if target is None or target.get_string() == 'toggle': mode = self.old_pointer_mode if self.pointer_mode == PointerMode.CONTINUOUS else PointerMode.CONTINUOUS else: mode = PointerMode[target.get_string().upper()] self.activate_pointermode(mode) action.change_state(GLib.Variant.new_string(mode.name.lower())) def render_pointer(self, cairo_context, ww, wh): """ Draw the laser pointer on screen. Args: cairo_context (:class:`~cairo.Context`): The canvas on which to render the pointer ww (`int`): The widget width wh (`int`): The widget height """ if self.show_pointer: x = ww * self.pointer_pos[0] - self.pointer.get_width() / 2 y = wh * self.pointer_pos[1] - self.pointer.get_height() / 2 Gdk.cairo_set_source_pixbuf(cairo_context, self.pointer, x, y) cairo_context.paint() def track_pointer(self, widget, event): """ Move the laser pointer at the mouse location. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. Returns: `bool`: whether the event was consumed """ if self.show_pointer: ww, wh = widget.get_allocated_width(), widget.get_allocated_height() ex, ey = event.get_coords() self.pointer_pos = (ex / ww, ey / wh) self.redraw_current_slide() return True else: return False def track_enter_leave(self, widget, event): """ Switches laser off/on in continuous mode on leave/enter slides. In continuous mode, the laser pointer is switched off when the mouse leaves the slide (otherwise the laser pointer "sticks" to the edge of the slide). It is switched on again when the mouse reenters the slide. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. Returns: `bool`: whether the event was consumed """ # Only handle enter/leave events on one of the current slides if self.pointer_mode != PointerMode.CONTINUOUS or widget not in [self.c_da, self.p_da_cur]: return False if event.type == Gdk.EventType.ENTER_NOTIFY: self.show_pointer = True extras.Cursor.set_cursor(widget, 'invisible') elif event.type == Gdk.EventType.LEAVE_NOTIFY: self.show_pointer = False extras.Cursor.set_cursor(widget, 'parent') self.redraw_current_slide() return True def toggle_pointer(self, widget, event): """ Track events defining when the laser is pointing. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. Returns: `bool`: whether the event was consumed """ if self.pointer_mode in {PointerMode.DISABLED, PointerMode.CONTINUOUS}: return False ctrl_pressed = event.get_state() & Gdk.ModifierType.CONTROL_MASK if ctrl_pressed and event.type == Gdk.EventType.BUTTON_PRESS: self.show_pointer = True extras.Cursor.set_cursor(widget, 'invisible') # Immediately place & draw the pointer return self.track_pointer(widget, event) elif self.show_pointer and event.type == Gdk.EventType.BUTTON_RELEASE: self.show_pointer = False extras.Cursor.set_cursor(widget, 'parent') self.redraw_current_slide() return True else: return False pympress-1.7.1/pympress/scribble.py000066400000000000000000000736661415371354200174520ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # pointer.py # # Copyright 2017 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.scribble` -- Manage user drawings on the current slide --------------------------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import math import gi import cairo gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GLib from pympress import builder, extras, util class Scribbler(builder.Builder): """ UI that allows to draw free-hand on top of the current slide. Args: config (:class:`~pympress.config.Config`): A config object containing preferences builder (:class:`~pympress.builder.Builder`): A builder from which to load widgets notes_mode (`bool`): The current notes mode, i.e. whether we display the notes on second slide """ #: Whether we are displaying the interface to scribble on screen and the overlays containing said scribbles scribbling_mode = False #: `list` of scribbles to be drawn, as tuples of color :class:`~Gdk.RGBA`, width `int`, and a `list` of points. scribble_list = [] #: `list` of undone scribbles to possibly redo scribble_redo_list = [] #: Whether the current mouse movements are drawing strokes or should be ignored scribble_drawing = False #: :class:`~Gdk.RGBA` current color of the scribbling tool scribble_color = Gdk.RGBA() #: `int` current stroke width of the scribbling tool scribble_width = 1 #: :class:`~Gtk.HBox` that replaces normal panes when scribbling is on, contains buttons and scribble drawing area. scribble_overlay = None #: :class:`~Gtk.DrawingArea` for the scribbles in the Presenter window. Actually redraws the slide. scribble_p_da = None #: :class:`~Gtk.EventBox` for the scribbling in the Content window, captures freehand drawing scribble_c_eb = None #: :class:`~Gtk.EventBox` for the scribbling in the Presenter window, captures freehand drawing scribble_p_eb = None #: :class:`~Gtk.AspectFrame` for the slide in the Presenter's highlight mode scribble_p_frame = None #: The :class:`~Gtk.DrawingArea` in the content window c_da = None #: The :class:`~Gtk.ColorButton` selecting the color of the pen scribble_color_selector = None #: The :class:`~Gtk.Scale` selecting the size of the pen scribble_width_selector = None #: The `list` containing the radio buttons :class:`~Gtk.ModelButton` scribble_preset_buttons = [] #: The position of the mouse on the slide as `tuple` of `float` mouse_pos = None #: A :class:`~cairo.Surface` to hold drawn highlights scribble_cache = None #: The next scribble to render (i.e. that is not rendered in cache) next_render = 0 #: :class:`~Gtk.Button` for removing the last drawn scribble scribble_undo = None #: :class:`~Gtk.Button` for drawing the last removed scribble scribble_redo = None #: :class:`~Gtk.Button` for removing all drawn scribbles scribble_clear = None #: A :class:`~Gtk.OffscreenWindow` where we render the scribbling interface when it's not shown off_render = None #: :class:`~Gtk.Box` in the Presenter window, where we insert scribbling. p_central = None #: :class:`~Gtk.Button` that is clicked to stop zooming, unsensitive when there is no zooming zoom_stop_button = None #: callback, to be connected to :func:`~pympress.surfacecache.SurfaceCache.resize_widget` resize_cache = lambda *args: None #: callback, to be connected to :func:`~pympress.ui.UI.on_draw` on_draw = lambda *args: None #: callback, to be connected to :func:`~pympress.ui.UI.track_motions` track_motions = lambda *args: None #: callback, to be connected to :func:`~pympress.ui.UI.track_clicks` track_clicks = lambda *args: None #: callback, to be connected to :func:`~pympress.ui.UI.load_layout` load_layout = lambda *args: None #: callback, to be connected to :func:`~pympress.ui.UI.redraw_current_slide` redraw_current_slide = lambda *args: None #: callback, to be connected to :func:`~pympress.extras.Zoom.get_slide_point` get_slide_point = lambda *args: None #: callback, to be connected to :func:`~pympress.extras.Zoom.start_zooming` start_zooming = lambda *args: None #: callback, to be connected to :func:`~pympress.extras.Zoom.stop_zooming` stop_zooming = lambda *args: None #: `int` that is the currently selected element active_preset = -1 #: The :class:`~Gio.Action` that contains the currently selected pen pen_action = None #: `str` which is the mode for scribbling, one of 3 possile values: # global means one set of scribbles for the whole document # single-page means we manage a single page of scribbles, and clear everything on page change (historical behaviour) # per-page means we manage a set of scribbles per document page, and clear or restore them on page change # per-label means we manage a set of scribbles per document page, but defined by label and not page number highlight_mode = 'single-page' #: `dict` of scribbles per page remembered_scribbles = {} #: `tuple` of (`int`, `str`) indicating the current page number and label current_page = (None, None) #: `str` indicating the current layout of the highlight toolbar tools_orientation = 'vertical' #: :class:`~Gtk.Box` containing the presets preset_toolbar = None #: :class:`~Gtk.Box` containing the scribble buttons scribble_toolbar = None #: :class:`~Gtk.Box` containing the scribble color and width selectors scribble_color_toolbox = None def __init__(self, config, builder, notes_mode): super(Scribbler, self).__init__() self.load_ui('highlight') builder.load_widgets(self) self.get_application().add_window(self.off_render) self.on_draw = builder.get_callback_handler('on_draw') self.track_motions = builder.get_callback_handler('track_motions') self.track_clicks = builder.get_callback_handler('track_clicks') self.load_layout = builder.get_callback_handler('load_layout') self.redraw_current_slide = builder.get_callback_handler('redraw_current_slide') self.resize_cache = builder.get_callback_handler('cache.resize_widget') self.get_slide_point = builder.get_callback_handler('zoom.get_slide_point') self.start_zooming = builder.get_callback_handler('zoom.start_zooming') self.stop_zooming = builder.get_callback_handler('zoom.stop_zooming') self.connect_signals(self) self.config = config # Prepare cairo surfaces for markers, with 3 different marker sizes, and for eraser ms = [1, 2, 3] icons = [cairo.ImageSurface.create_from_png(util.get_icon_path('marker_{}.png'.format(n))) for n in ms] masks = [cairo.ImageSurface.create_from_png(util.get_icon_path('marker_fill_{}.png'.format(n))) for n in ms] self.marker_surfaces = list(zip(icons, masks)) self.eraser_surface = cairo.ImageSurface.create_from_png(str(util.get_icon_path('eraser.png'))) # Load color and active pen preferences. Pen 0 is the eraser. self.color_width = [(Gdk.RGBA(0, 0, 0, 0), 150)] + list(zip( [self.parse_color(config.get('highlight', 'color_{}'.format(pen))) for pen in range(1, 10)], [config.getint('highlight', 'width_{}'.format(pen)) for pen in range(1, 10)], )) self.scribble_preset_buttons = [ self.get_object('pen_preset_{}'.format(pen) if pen else 'eraser') for pen in range(10) ] self.tools_orientation = self.config.get('layout', 'highlight_tools') self.adjust_tools_orientation() active_pen = config.get('highlight', 'active_pen') self.setup_actions({ 'highlight': dict(activate=self.switch_scribbling, state=False), 'highlight-use-pen': dict(activate=self.load_preset, state=active_pen, parameter_type=str, enabled=False), 'highlight-clear': dict(activate=self.clear_scribble), 'highlight-redo': dict(activate=self.redo_scribble), 'highlight-undo': dict(activate=self.pop_scribble), 'highlight-mode': dict(activate=self.set_mode, state=self.highlight_mode, parameter_type=str), 'highlight-tools-orientation': dict(activate=self.set_tools_orientation, state=self.tools_orientation, parameter_type=str), }) self.pen_action = self.get_application().lookup_action('highlight-use-pen') self.load_preset(self.pen_action, int(active_pen) if active_pen.isnumeric() else 0) self.set_mode(None, GLib.Variant.new_string(config.get('highlight', 'mode'))) def set_mode(self, gaction, param): """ Change the mode of clearing and restoring highlights Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the new mode as a string wrapped in a GLib.Variant """ new_mode = param.get_string() if new_mode not in {'single-page', 'global', 'per-page', 'per-label'}: return False self.get_application().lookup_action('highlight-mode').change_state(GLib.Variant.new_string(new_mode)) self.highlight_mode = new_mode self.config.set('highlight', 'mode', self.highlight_mode) self.remembered_scribbles.clear() return True def try_cancel(self): """ Cancel scribbling, if it is enabled. Returns: `bool`: `True` if scribbling got cancelled, `False` if it was already disabled. """ if not self.scribbling_mode: return False self.disable_scribbling() return True @staticmethod def parse_color(text): """ Transform a string to a Gdk object in a single function call Args: text (`str`): A string describing a color Returns: :class:`~Gdk.RGBA`: A new color object parsed from the string """ color = Gdk.RGBA() color.parse(text) return color def points_to_curves(self, points): """ Transform a list of points from scribbles to bezier curves Returns: `list`: control points of a bezier curves to draw """ curves = [] if len(points) <= 2: return curves c1 = points[1] for c2, pt in zip(points[2:-1:2], points[3:-1:2]): half_c2pt = (pt[0] - c2[0]) / 2, (pt[1] - c2[1]) / 2 curves.append((*c1, c2[0] + half_c2pt[0], c2[1] + half_c2pt[1], *pt)) c1 = (pt[0] + half_c2pt[0], pt[1] + half_c2pt[1]) if len(points) % 2 == 0: curves.append((*c1, *points[-2], *points[-1])) return curves def track_scribble(self, widget, event): """ Draw the scribble following the mouse's moves. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. Returns: `bool`: whether the event was consumed """ pos = self.get_slide_point(widget, event) if self.scribble_drawing: self.scribble_list[-1][-1].append(pos) self.scribble_redo_list.clear() self.adjust_buttons() self.mouse_pos = pos self.redraw_current_slide() return self.scribble_drawing def toggle_scribble(self, widget, event): """ Start/stop drawing scribbles. Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. Returns: `bool`: whether the event was consumed """ if not self.scribbling_mode: return False if event.get_event_type() == Gdk.EventType.BUTTON_PRESS: self.scribble_list.append((self.scribble_color, self.scribble_width, [])) self.scribble_drawing = True return self.track_scribble(widget, event) elif event.get_event_type() == Gdk.EventType.BUTTON_RELEASE: self.scribble_drawing = False self.prerender() return True return False def reset_scribble_cache(self): """ Clear the cached scribbles. """ window = self.c_da.get_window() if window is None: return ValueError('Cannot initialize scribble acche without drawing area window') scale = window.get_scale_factor() ww, wh = self.c_da.get_allocated_width() * scale, self.c_da.get_allocated_height() * scale try: self.scribble_cache = window.create_similar_image_surface(cairo.Format.ARGB32, ww, wh, scale) except ValueError: logger.exception('Error creating highlight cache') self.next_render = 0 def prerender(self): """ Clear scribbles to cached. """ if self.scribble_cache is None: try: self.reset_scribble_cache() except ValueError as e: logger.info(e) return if self.scribble_cache is None: self.next_render = 0 return ww, wh = self.scribble_cache.get_width(), self.scribble_cache.get_height() monitor = self.c_da.get_display().get_monitor_at_window(self.c_da.get_parent_window()).get_geometry() pen_scale_factor = max(ww / monitor.width, wh / monitor.height) # or sqrt of product cairo_context = cairo.Context(self.scribble_cache) cairo_context.set_line_cap(cairo.LINE_CAP_ROUND) draw = slice(self.next_render, -1 if self.scribble_drawing else None) for color, width, points in self.scribble_list[draw]: self.render_scribble(cairo_context, color, width * pen_scale_factor, [(x * ww, y * wh) for x, y in points]) del cairo_context self.next_render = len(self.scribble_list) + (draw.stop if draw.stop else 0) def render_scribble(self, cairo_context, color, width, points): """ Draw a single scribble, i.e. a bezier curve, on the cairo context Args: cairo_context (:class:`~cairo.Context`): The canvas on which to render the drawings color (:class:`~Gdk.RGBA`): The color of the scribble width (`float`): The width of the curve points (`list`): The control points of the curve, scaled to the surface. Returns: :class:`~cairo.Path`: A copy of the path that was drawn """ if not points: return # alpha == 0 -> Eraser mode cairo_context.set_operator(cairo.OPERATOR_OVER if color.alpha else cairo.OPERATOR_CLEAR) cairo_context.set_source_rgba(*color) cairo_context.set_line_width(width) cairo_context.move_to(*points[0]) for curve in self.points_to_curves(points): cairo_context.curve_to(*curve) path = cairo_context.copy_path() cairo_context.line_to(*points[-1]) cairo_context.stroke() return path def draw_scribble(self, widget, cairo_context): """ Perform the drawings by user. Args: widget (:class:`~Gtk.DrawingArea`): The widget where to draw the scribbles. cairo_context (:class:`~cairo.Context`): The canvas on which to render the drawings """ ww, wh = widget.get_allocated_width(), widget.get_allocated_height() cw, ch = self.scribble_cache.get_width(), self.scribble_cache.get_height() cairo_context.push_group() cairo_context.save() cairo_context.scale(ww / cw, wh / ch) cairo_context.set_source_surface(self.scribble_cache) cairo_context.paint() cairo_context.restore() monitor = widget.get_display().get_monitor_at_window(widget.get_parent_window()).get_geometry() pen_scale_factor = max(ww / monitor.width, wh / monitor.height) # or sqrt of product if self.scribble_drawing: cairo_context.set_line_cap(cairo.LINE_CAP_ROUND) color, width, points = self.scribble_list[-1] self.render_scribble(cairo_context, color, width * pen_scale_factor, [(x * ww, y * wh) for x, y in points]) cairo_context.pop_group_to_source() cairo_context.paint() if widget.get_name() == 'scribble_p_da' and self.mouse_pos is not None: cairo_context.set_source_rgba(0, 0, 0, 1) cairo_context.set_line_width(1) mx, my = self.mouse_pos cairo_context.arc(mx * ww, my * wh, self.scribble_width * pen_scale_factor / 2, 0, 2 * math.pi) cairo_context.stroke_preserve() cairo_context.set_source_rgba(*list(self.scribble_color)[:3], self.scribble_color.alpha * .5) cairo_context.close_path() cairo_context.fill() def update_color(self, widget): """ Callback for the color chooser button, to set scribbling color. Args: widget (:class:`~Gtk.ColorButton`): the clicked button to trigger this event, if any """ self.scribble_color = widget.get_rgba() self.update_active_color_width() def update_width(self, widget, event, value): """ Callback for the width chooser slider, to set scribbling width. Args: widget (:class:`~Gtk.Scale`): The slider control used to select the scribble width event (:class:`~Gdk.Event`): the GTK event triggering this update. value (`int`): the width of the scribbles to be drawn """ self.scribble_width = max(5, min(90, int(value))) self.update_active_color_width() def update_active_color_width(self): """ Update modifications to the active scribble color and width, on the pen button and config object """ if not self.active_preset: return self.color_width[self.active_preset] = self.scribble_color, self.scribble_width self.scribble_preset_buttons[self.active_preset].queue_draw() pen = self.active_preset self.config.set('highlight', 'color_{}'.format(pen), self.scribble_color.to_string()) self.config.set('highlight', 'width_{}'.format(pen), str(self.scribble_width)) def adjust_buttons(self): """ Properly enable and disable buttons based on scribblings lists. """ self.scribble_undo.set_sensitive(bool(self.scribble_list)) self.scribble_clear.set_sensitive(bool(self.scribble_list)) self.scribble_redo.set_sensitive(bool(self.scribble_redo_list)) def clear_scribble(self, *args): """ Callback for the scribble clear button, to remove all scribbles. """ self.scribble_list.clear() self.reset_scribble_cache() self.redraw_current_slide() self.adjust_buttons() def page_change(self, page_number, page_label): """ Called when we change pages, to clear or restore scribbles Args: page_number (`int`): The number of the new page page_label (`str`): The label of the new page """ if self.highlight_mode == 'per-page': current_page = self.current_page[0] new_page = page_number elif self.highlight_mode == 'per-label': current_page = self.current_page[1] new_page = page_label # Remember whatever the current mode, to facilitate switching modes self.current_page = (page_number, page_label) if self.highlight_mode == 'global': return elif self.highlight_mode == 'single-page': return self.clear_scribble() else: # Now optionally save the current scribbles if current_page is not None and self.scribble_list: self.remembered_scribbles[current_page] = self.scribble_list.copy() self.scribble_list = self.remembered_scribbles.pop(new_page, []) self.reset_scribble_cache() self.adjust_buttons() self.prerender() self.redraw_current_slide() def pop_scribble(self, *args): """ Callback for the scribble undo button, to undo the last scribble. """ if self.scribble_list: self.scribble_redo_list.append(self.scribble_list.pop()) self.adjust_buttons() self.reset_scribble_cache() self.prerender() self.redraw_current_slide() def redo_scribble(self, *args): """ Callback for the scribble undo button, to undo the last scribble. """ if self.scribble_redo_list: self.scribble_list.append(self.scribble_redo_list.pop()) self.adjust_buttons() self.prerender() self.redraw_current_slide() def on_configure_da(self, widget, event): """ Transfer configure resize to the cache. Args: widget (:class:`~Gtk.Widget`): the widget which has been resized event (:class:`~Gdk.Event`): the GTK event, which contains the new dimensions of the widget """ # Don't trust those if not event.send_event: return self.resize_cache(widget.get_name(), event.width, event.height) def set_tools_orientation(self, gaction, target): """ Changes the orientation of the highlighting tool box. Args: gaction (:class:`~Gio.Action`): the action triggering the call target (:class:`~GLib.Variant`): the new orientation to set, as a string wrapped in a GLib.Variant Returns: `bool`: whether the preset was loaded """ orientation = target.get_string() if orientation == self.tools_orientation: return False elif orientation not in ['horizontal', 'vertical']: logger.error('Unexpected highlight-tools orientation {}'.format(orientation)) return False self.tools_orientation = orientation self.adjust_tools_orientation() gaction.change_state(target) self.config.set('layout', 'highlight_tools', self.tools_orientation) def adjust_tools_orientation(self): """ Actually change the highlight tool elements orientations according to self.tools_orientation """ orientation = Gtk.Orientation.VERTICAL if self.tools_orientation == 'vertical' else Gtk.Orientation.HORIZONTAL self.preset_toolbar.set_orientation(orientation) self.scribble_toolbar.set_orientation(orientation) self.scribble_color_toolbox.set_orientation(orientation) self.scribble_width_selector.set_orientation(orientation) w, h = sorted(self.scribble_width_selector.get_size_request(), reverse=self.tools_orientation != 'vertical') self.scribble_width_selector.set_size_request(w, h) # NB the parent container is layed out perpendicularly to its contents self.scribble_overlay.set_orientation(Gtk.Orientation.HORIZONTAL if self.tools_orientation == 'vertical' else Gtk.Orientation.VERTICAL) def switch_scribbling(self, gaction, target=None): """ Starts the mode where one can read on top of the screen. Args: Returns: `bool`: whether the event was consumed """ if target is not None and target == self.scribbling_mode: return False # Perform the state toggle if self.scribbling_mode: return self.disable_scribbling() else: return self.enable_scribbling() def enable_scribbling(self): """ Enable the scribbling mode. Returns: `bool`: whether it was possible to enable (thus if it was not enabled already) """ if self.scribbling_mode: return False self.off_render.remove(self.scribble_overlay) self.load_layout('highlight') self.p_central.queue_draw() self.scribble_overlay.queue_draw() self.scribbling_mode = True self.get_application().lookup_action('highlight').change_state(GLib.Variant.new_boolean(self.scribbling_mode)) self.pen_action.set_enabled(self.scribbling_mode) self.p_central.queue_draw() extras.Cursor.set_cursor(self.scribble_p_da, 'invisible') return True def disable_scribbling(self): """ Disable the scribbling mode. Returns: `bool`: whether it was possible to disable (thus if it was not disabled already) """ if not self.scribbling_mode: return False self.scribbling_mode = False extras.Cursor.set_cursor(self.scribble_p_da, 'default') self.load_layout(None) self.off_render.add(self.scribble_overlay) self.get_application().lookup_action('highlight').change_state(GLib.Variant.new_boolean(self.scribbling_mode)) self.pen_action.set_enabled(self.scribbling_mode) self.p_central.queue_draw() extras.Cursor.set_cursor(self.p_central) self.mouse_pos = None return True def load_preset(self, gaction=None, target=None): """ Loads the preset color of a given number or designed by a given widget, as an event handler. Args: gaction (:class:`~Gio.Action`): the action triggering the call target (:class:`~GLib.Variant`): the new preset to load, as a string wrapped in a GLib.Variant Returns: `bool`: whether the preset was loaded """ if type(target) == int: self.active_preset = target else: self.active_preset = int(target.get_string()) if target.get_string() != 'eraser' else 0 target = str(self.active_preset) if self.active_preset else 'eraser' self.config.set('highlight', 'active_pen', target) self.pen_action.change_state(GLib.Variant.new_string(target)) self.scribble_color, self.scribble_width = self.color_width[self.active_preset] # Presenter-side setup self.scribble_color_selector.set_rgba(self.scribble_color) self.scribble_width_selector.set_value(self.scribble_width) self.scribble_color_selector.set_sensitive(target != 'eraser') self.scribble_width_selector.set_sensitive(target != 'eraser') return True def on_eraser_button_draw(self, widget, cairo_context): """ Handle drawing the eraser button. Args: widget (:class:`~Gtk.Widget`): the widget to update cairo_context (:class:`~cairo.Context`): the Cairo context (or `None` if called directly) """ cairo_context.push_group() scale = widget.get_allocated_height() / self.eraser_surface.get_height() cairo_context.scale(scale, scale) cairo_context.set_source_surface(self.eraser_surface) cairo_context.paint() cairo_context.pop_group_to_source() cairo_context.paint() def on_preset_button_draw(self, widget, cairo_context): """ Handle drawing the marker/pencil buttons, with appropriate thickness and color. Args: widget (:class:`~Gtk.Widget`): the widget to update cairo_context (:class:`~cairo.Context`): the Cairo context (or `None` if called directly) """ button_number = int(widget.get_name().split('_')[-1]) color, width = self.color_width[button_number] icon, mask = self.marker_surfaces[int((width - 1) / 30)] ww, wh = widget.get_allocated_width(), widget.get_allocated_height() scale = wh / icon.get_height() dw, dh = self.scribble_p_da.get_allocated_width(), self.scribble_p_da.get_allocated_height() monitor = widget.get_display().get_monitor_at_window(widget.get_parent_window()).get_geometry() pen_scale_factor = max(dw / monitor.width, dh / monitor.height) width *= pen_scale_factor cairo_context.push_group() # A line demonstrating the scribble style cairo_context.set_source_rgba(*color) cairo_context.set_line_width(width) cairo_context.move_to(0, wh - width / 2) cairo_context.line_to(ww, wh - width / 2) cairo_context.stroke() cairo_context.set_operator(cairo.OPERATOR_DEST_OUT) # Clip the line to the lower triangle cairo_context.set_source_rgba(0, 0, 0, 1) cairo_context.set_line_width(0) cairo_context.move_to(0, 0) cairo_context.line_to(0, wh) cairo_context.line_to(ww, 0) cairo_context.close_path() cairo_context.fill() # Also clip the colored part of the marker cairo_context.scale(scale, scale) cairo_context.set_source_surface(mask) cairo_context.paint() cairo_context.pop_group_to_source() cairo_context.paint() cairo_context.push_group() # Fill with desired color cairo_context.set_source_rgba(*color) cairo_context.rectangle(0, 0, ww, wh) cairo_context.fill() # Transform for surfaces cairo_context.scale(scale, scale) # Clip color to the mask cairo_context.set_operator(cairo.OPERATOR_DEST_IN) cairo_context.set_source_surface(mask) cairo_context.paint() # Add the rest of the marker cairo_context.set_operator(cairo.OPERATOR_OVER) cairo_context.set_source_surface(icon) cairo_context.paint() cairo_context.pop_group_to_source() cairo_context.paint() pympress-1.7.1/pympress/share/000077500000000000000000000000001415371354200163735ustar00rootroot00000000000000pympress-1.7.1/pympress/share/applications/000077500000000000000000000000001415371354200210615ustar00rootroot00000000000000pympress-1.7.1/pympress/share/applications/pympress.desktop000066400000000000000000000004501415371354200243350ustar00rootroot00000000000000[Desktop Entry] Categories=Office;Viewer;Presentation;GTK; Keywords=Presentation;Dual-Screen;Beamer; Comment=A simple yet powerful PDF reader designed for dual-screen presentations Exec=pympress %f Icon=pympress MimeType=application/pdf; Name=pympress Terminal=false Type=Application Version=1.0 pympress-1.7.1/pympress/share/css/000077500000000000000000000000001415371354200171635ustar00rootroot00000000000000pympress-1.7.1/pympress/share/css/default.css000066400000000000000000000026311415371354200213230ustar00rootroot00000000000000#p_win { } #c_win.black, #c_frame.black { background-color: #000; } #c_win.white , #c_frame.white { background-color: #fff; } #annotations_textview { font-size: 1.4em; } #p_win GtkPaned .pane-separator { background-color: #ccc; border-radius: 2px; } #p_win GtkFrame { } #p_win GtkTreeView { border-style: none; background-color: transparent; font-size: 16pt; } #label_time, #label_ett, #frame_clock { font-family: "monospace"; } #frame_ett { /* similar to button padding to add some room */ margin-left: 3px; margin-right: 3px; } #frame_cur { min-width: 15em; } #frame_ett { min-width: 6em; } #frame_clock { min-width: 8em; } .toolbar { background: @theme_base_color } #scribble_overlay { background: @theme_bg_color } #scribble_overlay modelbutton.pen-preset radio { margin: 0 30px 25px 0; } .frame-label { font-size:1.2em; } .big-info-label { font-size: 1.6em; } .info-label { font-size: 1.4em; opacity: 0.80; } .big-info-label > .frame-label { font-size:0.7em; } .info-label > .frame-label { font-size:0.8em; } .ett-reached { color: @success_color; } .ett-info { color: @warning_color; } .ett-warn { color: @error_color; } .time-warn { animation: blinker 250ms cubic-bezier(.5, 0, 1, 1) infinite alternate; } @keyframes blinker { from { opacity: 1; } to { opacity: 0; } } #MenuBar, #MenuBar * { } #MenuBar *:hover { } pympress-1.7.1/pympress/share/defaults.conf000066400000000000000000000072371415371354200210620ustar00rootroot00000000000000[content] xalign = 0.5 yalign = 0.5 geometry = 800x600 start_blanked = off start_fullscreen = on white_blanking = off [presenter] geometry = 800x600 start_fullscreen = off pointer = red pointer_mode = manual show_bigbuttons = off show_annotations = off scroll_number = off slide_ratio = 0.75 next_slide_count = 1 [layout] plain = { "resizeable": true, "children": [ "current", { "resizeable": true, "children": [ "next", "annotations" ], "proportions": [ 0.55, 0.44999999999999996 ], "orientation": "vertical" } ], "proportions": [ 0.6697916666666667, 0.3302083333333333 ], "orientation": "horizontal" } highlight_tools = vertical highlight = { "resizeable": true, "orientation": "horizontal", "children": [ "highlight", { "resizeable": true, "orientation": "vertical", "children": [ "next", "annotations" ], "proportions": [ 0.55, 0.45 ] } ], "proportions": [ 0.67, 0.33 ] } notes = { "resizeable": true, "children": [ "notes", { "resizeable": false, "children": [ "current", "next" ], "orientation": "vertical" } ], "proportions": [ 0.6, 0.4 ], "orientation": "horizontal" } note_pages = { "resizeable": true, "children": [ "notes", { "resizeable": false, "children": [ "next", "annotations" ], "orientation": "vertical" } ], "proportions": [ 0.6, 0.4 ], "orientation": "horizontal" } [notes position] horizontal = right vertical = bottom [cache] maxpages = 200 [highlight] color_1 = rgba(255,255,0,0.5) width_1 = 90 color_2 = rgba(128,255,0,0.5) width_2 = 90 color_3 = rgba(255,0,128,0.5) width_3 = 90 color_4 = rgba(48,48,255,0.5) width_4 = 90 color_5 = rgb(239,41,41) width_5 = 5 color_6 = rgb(0,96,255) width_6 = 5 color_7 = rgb(0,187,0) width_7 = 5 color_8 = rgb(0,0,0) width_8 = 5 color_9 = rgb(136,136,136) width_9 = 48 active_pen = 9 mode = single-page [gstreamer] enabled = on init_options = mime_types = [vlc] enabled = on init_options = --no-video-title-show,--no-xlib mime_types = [shortcuts] next-page = Right Down Page_Down space prev-page = Left Up Page_Up first-page = Home last-page = End next-label = Right Down Page_Down space prev-label = Left Up Page_Up hist-back = Left hist-forward = Right goto-page = g jumpto-label = j content-fullscreen = F11 f F5 l presenter-fullscreen = f zoom = z unzoom = u notes-mode = n annotations = a highlight = h swap-screens = s blank-screen = b quit = q validate-input = Return KP_Enter cancel-input = Escape # prompt to open file pick-file = o close-file = w save-file = s save-file-as = s pointer-mode::toggle = l pause-timer = p Pause reset-timer = r edit-talk-time = t timing-report = highlight-undo = z highlight-redo = r highlight-use-pen::1 = 1 highlight-use-pen::2 = 2 highlight-use-pen::3 = 3 highlight-use-pen::4 = 4 highlight-use-pen::5 = 5 highlight-use-pen::6 = 6 highlight-use-pen::7 = 7 highlight-use-pen::8 = 8 highlight-use-pen::9 = 9 highlight-use-pen::eraser = 0 highlight-clear = pympress-1.7.1/pympress/share/locale/000077500000000000000000000000001415371354200176325ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/babel_mapping.cfg000066400000000000000000000002411415371354200230700ustar00rootroot00000000000000[python: **.py] keywords = _ charset = utf-8 [glade: **.xml] keywords = translatable charset = utf-8 [glade: **.glade] keywords = translatable charset = utf-8 pympress-1.7.1/pympress/share/locale/cs/000077500000000000000000000000001415371354200202375ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/cs/LC_MESSAGES/000077500000000000000000000000001415371354200220245ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/cs/LC_MESSAGES/pympress.po000066400000000000000000000446661415371354200242660ustar00rootroot00000000000000msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: POEditor.com\n" "Project-Id-Version: pympress\n" "Language: cs\n" #: msgid "(and optionally seconds)" msgstr "(a případně sekundy)" #: msgid "(paused)" msgstr "(pozastaveno)" #: msgid "Adjust alignment of slides in projector screen" msgstr "Upravit zarovnání snímků na obrazovce projektoru" #: msgid "All files" msgstr "Všechny soubory" #: msgid "Annotations" msgstr "Popis" #: msgid "Big buttons" msgstr "Velká tlačítka" #: msgid "Cairo can not draw gif" msgstr "Cairo nemůže vykreslit gif" #: msgid "Clock" msgstr "Hodiny" #: msgid "Content and presenter window must not be on the same monitor if you start full screen!" msgstr "Obsah a okno s prezentací musí být na stejném monitoru pokud zapnete zobrazení na celé obrazovce!" #: msgid "Content blanked" msgstr "Prázdno" #: msgid "Content fullscreen" msgstr "Obsah na celou obrazovku" #: msgid "Contributors:" msgstr "Přispěvatelé:" #: msgid "Could not disable DPMS screen blanking: got status " msgstr "Nelze vypnout DPMS vyprázdnění obrazovky: stav " #: msgid "Could not enable DPMS screen blanking: got status " msgstr "Nelze zapnout DPMS vyprázdnění obrazovky: stav " #: msgid "Could not find the file \"{}\"" msgstr "Soubor \"{}\" nenalezen" #: msgid "Could not set screensaver status: got status " msgstr "Nelze nastavit stav spořiče obrazovky: stav " #: msgid "Current slide" msgstr "Aktuální snímek" #: msgid "ERROR: Gobject Introspections module is missing, make sure Gtk and pygobject are installed on your system." msgstr "ERROR: Chybí modul Gobject Introspections, ujistěte se, že Gtk a pygobject jsou nainstalovány." #: msgid "Error loading icon for about window" msgstr "Chyba při nahrávání ikony pro okno O programu" #: msgid "Error opening the file \"{}\"" msgstr "Chyba při otevírání souboru \"{}\"" #: msgid "For instructions, refer to https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" msgstr "Instrukce naleznete na https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" #: msgid "GtkImage gif player" msgstr "GtkImage přehrávač gifů" #: msgid "Highlight" msgstr "Zvýraznit" #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject" msgstr "Pokud využíváte virtualenv nebo anaconda můžete buďto využít balíčky distribuce nebo spusťte: pip install pygobject" #: msgid "Invalid log level \"{}\", try one of {}" msgstr "Neplatná úroveň logu \"{}\", vyzkoušejte jednu z těchto: {}" #: msgid "Invalid time (mm or mm:ss expected), got \"{}\"" msgstr "Neplatný čas (vyžadován tvar mm nebo mm:ss), zadáno \"{}\"" #: msgid "Media support uses {}." msgstr "Podpora médií využívá {}." #: msgid "Next slide" msgstr "Další snímek" #: msgid "No action was defined for this link" msgstr "Pro tento odkaz nebyla definována žádná akce" #: msgid "Not starting content or presenter window full screen because there is only one monitor" msgstr "Okno s obsahem nebo prezentací nebylo spuštěno na celou obrazovku, protože je dostupný pouze jeden monitor" #: msgid "Notes" msgstr "Poznámky" #: msgid "Notes position" msgstr "Pozice poznámek" #: msgid "Open _Recent" msgstr "Otevřít _poslední" #: msgid "Open..." msgstr "Otevřít..." #: msgid "Options:" msgstr "Nastavení:" #: msgid "Overrides the detection from the file." msgstr "Přepíše detekci ze souboru." #: msgid "PDF files" msgstr "PDF soubory" #: msgid "Pause" msgstr "Pauza" #: msgid "Play" msgstr "Spustit" #: msgid "Pointer" msgstr "Ukazovátko" #: msgid "Portable installation" msgstr "Přenosná instalace" #: msgid "Presentation timing breakdown" msgstr "Rozbor načasování prezentace" #: msgid "Presenter fullscreen" msgstr "Prezentace na celou obrazovku" #: msgid "Pympress Content" msgstr "Pympress Obsah" #: msgid "Pympress Presenter" msgstr "Pympress Prezentace" #: msgid "Pympress can not extract attached file" msgstr "Pympress nemůže extrahovat přiložený soubor" #: msgid "Pympress can not extract embedded media" msgstr "Pympress nemůže extrahovat vložená multimédia" #: msgid "Pympress can not find file " msgstr "Pympress nenašel soubor " #: msgid "Pympress can not interpret annotation of type:" msgstr "Pympress nemůže zobrazit popis typu:" #: msgid "Pympress does not recognize link type \"{}\"" msgstr "Pympress nerozpoznal typ odkazu \"{}\"" #: msgid "Pympress does not recognize link type \"{}\" to \"{}\"" msgstr "Pympress nerozpoznal typ odkazu \"{}\" až \"{}\"" #: msgid "Pympress does not yet support link type \"{}\"" msgstr "Pympress zatím nepodporuje odkaz typu \"{}\"" #: msgid "Pympress does not yet support link type \"{}\" to \"{}\"" msgstr "Pympress zatím nepodporuje odkaz typu \"{}\" až \"{}\"" #: msgid "Python version {}" msgstr "Python verze {}" #: msgid "Resources are loaded from " msgstr "Prostředky jsou nahrány z " #: msgid "Set level of verbosity in log file:" msgstr "Nastavit úroveň podrobnosti log souboru:" #: msgid "Set talk _Time" msgstr "Nastavit _Čas řeči" #: msgid "Set the position of notes on the pdf page (none, left, right, top, or bottom)." msgstr "Nastavit polohu poznámek na stránce pdf (žádné, vlevo, vpravo, nahoře nebo dole)." #: msgid "Slide number" msgstr "Snímek číslo" #: msgid "Some preferences are saved in " msgstr "Některé předvolby jsou uloženy v " #: msgid "Stop" msgstr "Zastavit" #: msgid "The estimated (intended) talk time in minutes" msgstr "Odhadovaná (plánovaná) doba prezentace v minutách" #: msgid "The log is written to " msgstr "Log je zapsán do " #: msgid "This help" msgstr "Tato pomoc" #: msgid "Time elapsed" msgstr "Uplynulý čas" #: msgid "Time estimation" msgstr "Odhad času" #: msgid "Timing breakdown" msgstr "Rozbor načasování" #: msgid "Unexpected action in index \"{}\"" msgstr "Neočekávaná akce v indexu \"{}\"" #: msgid "Unknow widget {} to be fullscreened, aborting." msgstr "Neznámý widget {} bude na maximalizován na celou obrazovku, ruším." #: msgid "Unsupported OS: can't enable/disable screensaver" msgstr "Nepodporovaný OS: nelze zapnout/vypnout spořič obrazovky" #: msgid "Usage: {} [options] " msgstr "Použití: {} [options] " #: msgid "Video support using {} is disabled." msgstr "Podpora videa přes {} je vypnuta." #: msgid "_Align content" msgstr "_Zarovnat obsah" #: msgid "_Annotations" msgstr "_Poznámky" #: msgid "_Blank screen" msgstr "_Prázdné plátno" #: msgid "_Blue" msgstr "_Modré" #: msgid "_Bottom" msgstr "_Dole" #: msgid "_Disabled" msgstr "_Vypnuto" #: msgid "_File" msgstr "_Soubor" #: msgid "_First" msgstr "_První" #: msgid "_Fullscreen" msgstr "_Celá obrazovka" #: msgid "_Go to..." msgstr "_Jít na..." #: msgid "_Green" msgstr "_Zelené" #: msgid "_Help" msgstr "_Nápověda" #: msgid "_Highlight" msgstr "_Zvýraznění" #: msgid "_Jump to label" msgstr "_Skočit na štítek" #: msgid "_Last" msgstr "_Poslední" #: msgid "_Left" msgstr "_Vlevo" #: msgid "_Manual" msgstr "_Manuál" #: msgid "_Navigation" msgstr "_Navigace" #: msgid "_Next" msgstr "_Další" #: msgid "_Notes mode" msgstr "_Mód s poznámkami" #: msgid "_Pause timer" msgstr "_Pozastavit stopky" #: msgid "_Permanent" msgstr "_Trvalý" #: msgid "_Presentation" msgstr "_Prezentace" #: msgid "_Previous" msgstr "_Předchozí" #: msgid "_Red" msgstr "_Červené" #: msgid "_Reset timer" msgstr "_Resetovat stopky" #: msgid "_Right" msgstr "_Vpravo" #: msgid "_Shortcuts" msgstr "_Zkratky" #: msgid "_Starting Configuration" msgstr "_Počáteční nastavení" #: msgid "_Swap screens" msgstr "_Prohodit obrazovky" #: msgid "_Top" msgstr "_Nahoře" #: msgid "_Undo zoom" msgstr "_Oddálit" #: msgid "_Zoom in" msgstr "_Přiblížit" #: msgid "access denied when trying to access screen saver settings in registry!" msgstr "přístup byl odepřen při pokusu o přístup k nastavení spořiče obrazovky v registru!" #: msgid "column" msgstr "sloupec" #: msgid "duration" msgstr "doba" #: msgid "name" msgstr "název" #: msgid "no action defined for this link!" msgstr "žádná akce není definována pro tento odkaz!" #: msgid "page label" msgstr "štítek stránky" #: msgid "pip will then download and compile pygobject, for which you need the Gtk headers (or development package)." msgstr "pip stáhne a zkompiluje pygobject, pro který jsou potřeba Gtk hlavičky (nebo vývojový balíček)" #: msgid "pympress is a little PDF reader written in Python using Poppler for PDF rendering and GTK for the GUI.\n" "" msgstr "pympress je malý prohlížeč PDF napsaný v Pythonu používající Poppler k zobrazení PDF a GTK pro GUI.\n" "" #: msgid "slide" msgstr "snímek" #: msgid "slide #" msgstr "snímek #" #: msgid "time" msgstr "čas" #: msgid "{}, {}, {}, {}, or {}" msgstr "{}, {}, {}, {}, nebo {}" #: msgid "Additional features" msgstr "Další možnosti" #: msgid "Blank screen" msgstr "Prázdná obrazovka" #: msgid "Cancel goto/jump/highlighting/zooming" msgstr "Zrušit jít na/skočit na/zvýraznění/přiblížení" #: msgid "Close file" msgstr "Zavřít soubor" #: msgid "First slide" msgstr "První snímek" #: msgid "Go back in slide history" msgstr "Jít vzad v historii snímků" #: msgid "Go forward in slide history" msgstr "Jít vpřed v historii snímků" #: msgid "Go to page number" msgstr "Jít na stránku číslo" #: msgid "Jump to page label" msgstr "Skočit na štítek stránky" #: msgid "Last slide" msgstr "Poslední snímek" #: msgid "Manage files" msgstr "Spravovat soubory" #: msgid "Navigating" msgstr "Navigace" #: msgid "Next slide with different label" msgstr "Další snímek s jiným štítkem" #: msgid "Open file" msgstr "Otevřít soubor" #: msgid "Play/pause timer" msgstr "Pustit/zastavit časovač" #: msgid "Presentation" msgstr "Prezentace" #: msgid "Previous slide" msgstr "Předchozí snímek" #: msgid "Previous slide with different label" msgstr "Předchozí snímek s jiným štítkem" #: msgid "Quit" msgstr "Odejít" #: msgid "Reset timer" msgstr "Restartovat časovač" #: msgid "Set estimated talk time" msgstr "Nastavit předpokládanou dobu trvání prezentace" #: msgid "Swap windows" msgstr "Vyměnit okna" #: msgid "Timers" msgstr "Časovače" #: msgid "Toggle annotations" msgstr "Zobrazit anotace" #: msgid "Toggle fullscreen" msgstr "Celá obrazovka" #: msgid "Toggle highlighting" msgstr "Zvýrazňování" #: msgid "Toggle laserpointer" msgstr "Přepnout ukazovátko" #: msgid "Toggle notes mode" msgstr "Mód poznámek" #: msgid "Undo highlight stroke" msgstr "Zrušit obrys zvýraznění" #: msgid "Unzoom" msgstr "Oddálit" #: msgid "Validate goto/jump destination" msgstr "Potvrdit cíl pro přejití/přeskočení" #: msgid "Windows" msgstr "Okna" #: msgid "Zoom" msgstr "Přiblížení" #: msgid "Unrecognized named destination: " msgstr "Pojmenovaný cíl nerozeznán: " #: msgid "Unsupported link clicked. " msgstr "Kliknuto na nepodporovaný odkaz. " #: msgid "Fullscreen Presentation running" msgstr "Běží prezentace na celou obrazovku" #: msgid "Caused by " msgstr "Způsobeno " #: msgid "Building FileWatcher" msgstr "Vytváření FileWatcher" #: msgid "Missing dependency: python \"{}\" package" msgstr "Chybí nutný balíček: python \"{}\" package" #: msgid "Monitoring of changes to reload files automatically is not available" msgstr "Sledování změn pro automatickou aktualizaci souborů není dostupné" #: msgid "ERROR: Gobject Introspections and/or pycairo module is missing, make sure Gtk, pygobject and pycairo are installed on your system." msgstr "ERROR: chybí Gobject Introspections a/nebo pycairo modul, ujistěte se, že Gtk, pygobject a pycairo jsou instalovány ve vašem systému." #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject pycairo" msgstr "Při použití virtualenv nebo anaconda prostředí můžete buďto zkusit povolit systémové balíčky, nebo spustit: pip install pygobject pycairo" #: msgid "pip will then download and compile pygobject, for which you need the Gtk and cairo headers (or development packages)." msgstr "Program pip poté stáhne a zkompiluje pygobject, pro které potřebujete Gtk a cairo hlavičky (nebo vývojové balíčky)." #: msgid "(none, left, right, top, or bottom)." msgstr "(žádný, vlevo, vpravo, nahoře nebo dole)." #: msgid "If using a virtualenv or anaconda, you can also try allowing system site packages." msgstr "Při použití virtualenv nebo anaconda prostředí můžete také zkusit povolit systémové balíčky." #: msgid "Set the position of notes on the pdf page" msgstr "Nastavit pozici poznámek na stránce pdf" #: msgid "Try your operating system’s package manager, or try running: pip install pygobject pycairo" msgstr "Zkuste použít správce balíčků Vašeho operačního systému, nebo spustit: pip install pygobject pycairo" #: msgid "pip will then download and compile pygobject and pycairo, for which you need the Gtk and cairo headers (or development packages)." msgstr "Program pip poté stáhne a zkompiluje pygobject a pycairo, pro které potřebujete Gtk a cairo hlavičky (nebo vývojové balíčky)" #: msgid "(none, left, right, top, bottom, or after)." msgstr "(žádný, vlevo, vpravo, nahoře, dole nebo za)." #: msgid "_After slide pages" msgstr "_Za stránkami slidů" #: msgid "_Bottom half of slide" msgstr "_Dolní část slidu" #: msgid "_Left half of slide" msgstr "_Levá polovina slidu" #: msgid "_Right half of slide" msgstr "_Pravá polovina slidu" #: msgid "_Top half of slide" msgstr "_Horní polovina slidu" #: msgid "Error parsing option from config file {}.{} \"{}\" to bool" msgstr "Chyba čtení z konfiguračního souboru {}.{} \"{}\" do booleanu" #: msgid "Error parsing option from config file {}.{} \"{}\" to float" msgstr "Chyba čtení z konfiguračního souboru {}.{} \"{}\" do desetiných čísel" #: msgid "Error parsing option from config file {}.{} \"{}\" to int" msgstr "Chyba čtení z konfiguračního souboru {}.{} \"{}\" do celých čísel" #: msgid "Blank/unblank content screen" msgstr "Prázdný/neprázdný obsah obrazovky" #: msgid "Close opened pympress instance" msgstr "Zavřít otevřenou pympress instanci" #: msgid "Print version and exit" msgstr "Vytisknout verzi a zavřít" #: msgid "Reset talk timer" msgstr "Resetovat časovače řeči" #: msgid "Toggle pause of talk timer" msgstr "Přepnutí pauzy časovače řeči" #: msgid "GdkPixbuf gif player" msgstr "GdkPixbuf přehrávač gifů" #: msgid "Media support loaded: " msgstr "Podpora médií načtena: " #: msgid "Media support using {} is disabled." msgstr "Podpora médií pomocí {} je zakázána." #: msgid "Media support: " msgstr "Podpora médií: " #: msgid "Align _content" msgstr "Zarovnat _obsah" #: msgid "Clear and restore per page label" msgstr "Vyčistit a obnovit podle popisku stránky" #: msgid "Clear and restore per page number" msgstr "Vyčistit a obnovit podle čísla stránky" #: msgid "Clear on page change" msgstr "Vyčistit při změně stránky" #: msgid "Highlight mode" msgstr "Zvýrazňovací režim" #: msgid "Never clear (manually only)" msgstr "Nikdy nečistit (pouze manuálně)" #: msgid "Tools below slide" msgstr "Nástroje pod slidem" #: msgid "Tools next to slide" msgstr "Nástroje vedle slidu" #: msgid "_Close" msgstr "_Zavřít" #: msgid "_Open" msgstr "_Otevřít" #: msgid "_Pointer" msgstr "_Ukazovátko" #: msgid "_Quit" msgstr "_Ukončit" #: msgid "Gtk.Application.inhibit failed preventing screensaver, trying hard disabling" msgstr "Gtk.Application.inhibit se nepodařilo předejít zapnutí spořiče obrazovky, pokus o jeho zakázání." #: msgid "Should not require hard enable/disable screensaver on Linux" msgstr "Nemělo by vyžadovat natvrdo povolit/zakázat spořič obrazovky na Linuxu" #: msgid "10th next slide" msgstr "" #: msgid "11th next slide" msgstr "" #: msgid "12th next slide" msgstr "" #: msgid "13th next slide" msgstr "" #: msgid "14th next slide" msgstr "" #: msgid "15th next slide" msgstr "" #: msgid "16th next slide" msgstr "" #: msgid "2nd next slide" msgstr "" #: msgid "3rd next slide" msgstr "" #: msgid "4th next slide" msgstr "" #: msgid "5th next slide" msgstr "" #: msgid "6th next slide" msgstr "" #: msgid "7th next slide" msgstr "" #: msgid "8th next slide" msgstr "" #: msgid "9th next slide" msgstr "" #: msgid "Edit layout" msgstr "" #: msgid "Plain layout, without notes mode" msgstr "" #: msgid "Unknown widget \"{}\" to draw" msgstr "" #: msgid "annotations (hideable)" msgstr "" #: msgid "box" msgstr "" #: msgid "current slide" msgstr "" #: msgid "horizontal" msgstr "" #: msgid "next slide(s)" msgstr "" #: msgid "next slides count" msgstr "" #: msgid "orientation" msgstr "" #: msgid "resizeable" msgstr "" #: msgid "vertical" msgstr "" #: msgid "widget" msgstr "" #: msgid "Layout for beamer notes on second screen (no current slide preview in notes)" msgstr "" #: msgid "Layout for libreoffice notes on separate pages (with current slide preview in notes)" msgstr "" #: msgid "Plain layout, without note slides" msgstr "" #: msgid "Overwrite" msgstr "" #: msgid "Overwrite changes instead of reloading?" msgstr "" #: msgid "Reload" msgstr "" #: msgid "Save as..." msgstr "" #: msgid "Save changes before closing?" msgstr "" #: msgid "Saving changes will overwrite the changed file!" msgstr "" #: msgid "The open file was modified outside of pympress but you have made unsaved changes." msgstr "" #: msgid "Unsaved changes" msgstr "" #: msgid "Unsaved changes will be lost" msgstr "" #: msgid "Unsaved changes will be lost." msgstr "" #: msgid "_Discard" msgstr "" #: msgid "_Save" msgstr "" #: msgid "_Save as" msgstr "" #: msgid "annotations" msgstr "" #: msgid "Auto" msgstr "" #: msgid "Choose parameters for automatically playing slides" msgstr "" #: msgid "Loop" msgstr "" #: msgid "Save file" msgstr "" #: msgid "Save file as" msgstr "" #: msgid "Time per slide (s):" msgstr "" #: msgid "_Automatic navigation" msgstr "" #: msgid "Note pages" msgstr "" #: msgid "Plain" msgstr "" #: msgid "notes" msgstr "" #: msgid "Highlighting" msgstr "" #: msgid "Layout to draw on the current slide" msgstr "" #: msgid "highlighting" msgstr "" pympress-1.7.1/pympress/share/locale/de/000077500000000000000000000000001415371354200202225ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/de/LC_MESSAGES/000077500000000000000000000000001415371354200220075ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/de/LC_MESSAGES/pympress.po000066400000000000000000000426561415371354200242460ustar00rootroot00000000000000msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: POEditor.com\n" "Project-Id-Version: pympress\n" "Language: de\n" #: msgid "(and optionally seconds)" msgstr "(und optional Sekunden)" #: msgid "(paused)" msgstr "(pausiert)" #: msgid "Adjust alignment of slides in projector screen" msgstr "Ausrichtung der Folien für den Projektor anpassen" #: msgid "All files" msgstr "Alle Dateien" #: msgid "Annotations" msgstr "Anmerkungen" #: msgid "Big buttons" msgstr "Große Buttons" #: msgid "Cairo can not draw gif" msgstr "Cairo kann das GIF nicht darstellen" #: msgid "Clock" msgstr "Uhr" #: msgid "Content and presenter window must not be on the same monitor if you start full screen!" msgstr "Inhalts- und Präsentator-Fenster dürfen beim Start im Vollbild-Modus nicht auf dem gleichen Monitor sein!" #: msgid "Content blanked" msgstr "Inhaltsanzeige deaktiviert" #: msgid "Content fullscreen" msgstr "Vollbildanzeige" #: msgid "Contributors:" msgstr "Beitragende:" #: msgid "Could not disable DPMS screen blanking: got status " msgstr "DPMS-Deaktivierung des Bildschirms kann nicht abgeschaltet werden: Status" #: msgid "Could not enable DPMS screen blanking: got status " msgstr "DPMS-Deaktivierung des Bildschirms kann nicht eingeschaltet werden: Status" #: msgid "Could not find the file \"{}\"" msgstr "Die Datei \"{}\" konnte nicht gefunden werden" #: msgid "Could not set screensaver status: got status " msgstr "Bildschirmschoner-Status kann nicht gesetzt werden: Status " #: msgid "Current slide" msgstr "Aktuelle Folie" #: msgid "ERROR: Gobject Introspections module is missing, make sure Gtk and pygobject are installed on your system." msgstr "FEHLER: Gobject Introspections Modul fehlt. Stelle sicher, dass Gtk und pygobject auf dem System installiert sind." #: msgid "Error loading icon for about window" msgstr "Fehler beim Laden des Icons für das \"Über\"-Fenster" #: msgid "Error opening the file \"{}\"" msgstr "Die Datei \"{}\" konnte nicht geöffnet werden" #: msgid "For instructions, refer to https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" msgstr "Hinweise dazu gibt es unter https://github.com/Cimbali/pympress/blob/master/README.md#dependencies (engl.)" #: msgid "GtkImage gif player" msgstr "Gif-Wiedergabe mit GtkImage" #: msgid "Highlight" msgstr "Hervorheben" #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject" msgstr "Bei der Verwendung von virtualenv oder anaconda können entweder systemweit verfügbare Pakete verwendet werden, ansonsten führe den Befehl aus: pip install pygobject" #: msgid "Invalid log level \"{}\", try one of {}" msgstr "Ungültiges Log-Level \"{}\", versuche eines aus {}" #: msgid "Invalid time (mm or mm:ss expected), got \"{}\"" msgstr "Ungültige Zeit (mm oder mm:ss erwartet), habe \"{}\"" #: msgid "Media support uses {}." msgstr "Videounterstützung verwendet {}." #: msgid "Next slide" msgstr "Nächste Folie" #: msgid "No action was defined for this link" msgstr "Für diesen Link wurde keine Aktion definiert" #: msgid "Not starting content or presenter window full screen because there is only one monitor" msgstr "Inhalts- und Präsentator-Fenster werden nicht im Vollbild gestartet, da nur ein Monitor vorhanden ist" #: msgid "Notes" msgstr "Notizen" #: msgid "Notes position" msgstr "Position der Notizen" #: msgid "Open _Recent" msgstr "_Kürzlich geöffnet" #: msgid "Open..." msgstr "Öffnen..." #: msgid "Options:" msgstr "Optionen:" #: msgid "Overrides the detection from the file." msgstr "Überschreibt die Erkennung anhand der Datei." #: msgid "PDF files" msgstr "PDF-Dateien" #: msgid "Pause" msgstr "Pause" #: msgid "Play" msgstr "Wiedergabe" #: msgid "Pointer" msgstr "Zeiger" #: msgid "Portable installation" msgstr "Portable Installation" #: msgid "Presentation timing breakdown" msgstr "Aufschlüsselung der Präsentationszeit" #: msgid "Presenter fullscreen" msgstr "Vollbild für Präsentator" #: msgid "Pympress Content" msgstr "Pympress-Inhalt" #: msgid "Pympress Presenter" msgstr "Pympress Präsentationen" #: msgid "Pympress can not extract attached file" msgstr "Pympress kann angehängte Datei nicht extrahieren" #: msgid "Pympress can not extract embedded media" msgstr "Pympress kann eingebettete Medien nicht extrahieren" #: msgid "Pympress can not find file " msgstr "Pympress kann die Datei nicht finden" #: msgid "Pympress can not interpret annotation of type:" msgstr "Pympress kann die Anmerkung dieses Typs nicht verarbeiten:" #: msgid "Pympress does not recognize link type \"{}\"" msgstr "Pympress kann den Link-Typ \"{}\" nicht erkennen" #: msgid "Pympress does not recognize link type \"{}\" to \"{}\"" msgstr "Pympress kann den Link-Typ \"{}\" nach \"{}\" nicht erkennen" #: msgid "Pympress does not yet support link type \"{}\"" msgstr "Pympress unterstützt den Link-Typ \"{}\" noch nicht" #: msgid "Pympress does not yet support link type \"{}\" to \"{}\"" msgstr "Pympress unterstützt den Link-Typ \"{}\" nach \"{}\" noch nicht" #: msgid "Python version {}" msgstr "Python Version {}" #: msgid "Resources are loaded from " msgstr "Daten werden geladen von " #: msgid "Set level of verbosity in log file:" msgstr "Verbositäts-Level für die Logdatei setzen:" #: msgid "Set talk _Time" msgstr "_Redezeit setzen" #: msgid "Set the position of notes on the pdf page (none, left, right, top, or bottom)." msgstr "Setzen der Position der Notizen auf der PDF-Seite (ohne, links, rechts, oben, oder unten)." #: msgid "Slide number" msgstr "Foliennummer" #: msgid "Some preferences are saved in " msgstr "Ein Teil der Einstellungen ist gespeichert unter" #: msgid "Stop" msgstr "Stop" #: msgid "The estimated (intended) talk time in minutes" msgstr "Die geschätzte (angestrebte) Redezeit in Minuten" #: msgid "The log is written to " msgstr "Der Log wird geschrieben nach " #: msgid "This help" msgstr "Diese Hilfe" #: msgid "Time elapsed" msgstr "Vergangene Zeit" #: msgid "Time estimation" msgstr "Geschätzte Zeit" #: msgid "Timing breakdown" msgstr "Zeitaufschlüsselung" #: msgid "Unexpected action in index \"{}\"" msgstr "Unerwartete Aktion bei Index \"{}\"" #: msgid "Unknow widget {} to be fullscreened, aborting." msgstr "Unbekanntes Widget {} für die Vollbildanzeige, Abbruch." #: msgid "Unsupported OS: can't enable/disable screensaver" msgstr "Nicht unterstütztes OS: Bildschirmschoner kann nicht aktiviert/deaktiviert werden" #: msgid "Usage: {} [options] " msgstr "Verwendung: {} [Optionen] " #: msgid "Video support using {} is disabled." msgstr "Videounterstützung auf Basis von {} ist deaktiviert." #: msgid "_Align content" msgstr "Inhalt _ausrichten" #: msgid "_Annotations" msgstr "_Anmerkungen" #: msgid "_Blank screen" msgstr "_Schwarzer Bildschirm" #: msgid "_Blue" msgstr "_Blau" #: msgid "_Bottom" msgstr "_Unten" #: msgid "_Disabled" msgstr "_Deaktiviert" #: msgid "_File" msgstr "_Datei" #: msgid "_First" msgstr "_Anfang" #: msgid "_Fullscreen" msgstr "_Vollbild" #: msgid "_Go to..." msgstr "_Springen zu..." #: msgid "_Green" msgstr "_Grün" #: msgid "_Help" msgstr "_Hilfe" #: msgid "_Highlight" msgstr "_Hervorheben" #: msgid "_Jump to label" msgstr "Zum Label sprin_gen" #: msgid "_Last" msgstr "_Ende" #: msgid "_Left" msgstr "_Links" #: msgid "_Manual" msgstr "_Manuell" #: msgid "_Navigation" msgstr "_Navigation" #: msgid "_Next" msgstr "_Nächste" #: msgid "_Notes mode" msgstr "_Notizmodus" #: msgid "_Pause timer" msgstr "Timer _pausieren" #: msgid "_Permanent" msgstr "_Ständig" #: msgid "_Presentation" msgstr "_Präsentation" #: msgid "_Previous" msgstr "_Vorherige" #: msgid "_Red" msgstr "_Rot" #: msgid "_Reset timer" msgstr "Timer _zurücksetzen" #: msgid "_Right" msgstr "_Rechts" #: msgid "_Shortcuts" msgstr "_Shortcut" #: msgid "_Starting Configuration" msgstr "_Startkonfiguration" #: msgid "_Swap screens" msgstr "Bildschirme _tauschen" #: msgid "_Top" msgstr "_Oben" #: msgid "_Undo zoom" msgstr "Zoom z_urücksetzen" #: msgid "_Zoom in" msgstr "Hinein_zoomen" #: msgid "access denied when trying to access screen saver settings in registry!" msgstr "Zugriff auf die Bildschirmschoner-Einstellungen der Registry nicht erlaubt!" #: msgid "column" msgstr "Spalte" #: msgid "duration" msgstr "Dauer" #: msgid "name" msgstr "Name" #: msgid "no action defined for this link!" msgstr "für diesen Link ist keine Aktion definiert!" #: msgid "page label" msgstr "Seitenlabel" #: msgid "pip will then download and compile pygobject, for which you need the Gtk headers (or development package)." msgstr "pip wird dann pygobject herunterladen und kompilieren, wozu die Gtk-Header benötigt werden (oder das Entwicklungspaket)." #: msgid "pympress is a little PDF reader written in Python using Poppler for PDF rendering and GTK for the GUI.\n" "" msgstr "pympress ist ein kleiner PDF-Reader, geschrieben in Python unter Verwendung von Poppler für die PDF-Anzeige und GTK für die GUI.\n" "" #: msgid "slide" msgstr "Folie" #: msgid "slide #" msgstr "Folie #" #: msgid "time" msgstr "Zeit" #: msgid "{}, {}, {}, {}, or {}" msgstr "{}, {}, {}, {}, oder {}" #: msgid "Additional features" msgstr "Weitere Funktionen" #: msgid "Blank screen" msgstr "_Leerer Bildschirm" #: msgid "Cancel goto/jump/highlighting/zooming" msgstr "Goto/Sprung/Hervorhebung/Zoom abbrechen" #: msgid "Close file" msgstr "Datei schließen" #: msgid "First slide" msgstr "Erste Folie" #: msgid "Go back in slide history" msgstr "Zurück im Folienverlauf" #: msgid "Go forward in slide history" msgstr "Vorwärts im Folienverlauf" #: msgid "Go to page number" msgstr "Wechseln zu Folie Nummer" #: msgid "Jump to page label" msgstr "Zum Seitenlabel springen" #: msgid "Last slide" msgstr "Letzte Folie" #: msgid "Manage files" msgstr "Dateien verwalten" #: msgid "Navigating" msgstr "Navigation" #: msgid "Next slide with different label" msgstr "Nächste Folie mit abweichendem Label" #: msgid "Open file" msgstr "Datei öffnen" #: msgid "Play/pause timer" msgstr "Timer starten/pausieren" #: msgid "Presentation" msgstr "Präsentation" #: msgid "Previous slide" msgstr "Vorherige Folie" #: msgid "Previous slide with different label" msgstr "Vorherige Folie mit abweichendem Label" #: msgid "Quit" msgstr "Beenden" #: msgid "Reset timer" msgstr "Timer zurücksetzen" #: msgid "Set estimated talk time" msgstr "Geschätzte Redezeit setzen" #: msgid "Swap windows" msgstr "Fenster tauschen" #: msgid "Timers" msgstr "Timer" #: msgid "Toggle annotations" msgstr "Anmerkungen umschalten" #: msgid "Toggle fullscreen" msgstr "Vollbild umschalten" #: msgid "Toggle highlighting" msgstr "Hervorhebungen umschalten" #: msgid "Toggle laserpointer" msgstr "Laserpointer umschalten" #: msgid "Toggle notes mode" msgstr "Notizmodus umschalten" #: msgid "Undo highlight stroke" msgstr "Hervorhebung rückgängig machen" #: msgid "Unzoom" msgstr "Zoom rückgängig machen" #: msgid "Validate goto/jump destination" msgstr "Goto-/Sprung-Ziel validieren" #: msgid "Windows" msgstr "Fenster" #: msgid "Zoom" msgstr "Zoom" #: msgid "Unrecognized named destination: " msgstr "Unbekanntes benanntes Ziel: " #: msgid "Unsupported link clicked. " msgstr "Nicht unterstützter Link wurde angeklickt. " #: msgid "Fullscreen Presentation running" msgstr "Vollbild-Präsentation läuft" #: msgid "Caused by " msgstr "Verursacht durch " #: msgid "Building FileWatcher" msgstr "" #: msgid "Missing dependency: python \"{}\" package" msgstr "fehlende Abhängigkeit: python \"{}\" Paket" #: msgid "Monitoring of changes to reload files automatically is not available" msgstr "" #: msgid "ERROR: Gobject Introspections and/or pycairo module is missing, make sure Gtk, pygobject and pycairo are installed on your system." msgstr "Fehler: Gobject Introspections und/oder pycairo Modul fehlt. Überprüfe ob Gtk, pygobject und pycairo auf deinem System installiert sind." #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject pycairo" msgstr "" #: msgid "pip will then download and compile pygobject, for which you need the Gtk and cairo headers (or development packages)." msgstr "pip wird dann pygoobject downloaden und compilieren, für das Gtk und cairo header (oder Entwicklungspakete) benötigt werden." #: msgid "(none, left, right, top, or bottom)." msgstr "(keine, links, rechts, oben oder unten)" #: msgid "If using a virtualenv or anaconda, you can also try allowing system site packages." msgstr "" #: msgid "Set the position of notes on the pdf page" msgstr "Positionierung der Notizen auf der PDF Seite" #: msgid "Try your operating system’s package manager, or try running: pip install pygobject pycairo" msgstr "Versuche die Paketverwaltung des Betriebssystem oder Versuche folgenden Befehl auszuführen: pip install pygobject pycairo" #: msgid "pip will then download and compile pygobject and pycairo, for which you need the Gtk and cairo headers (or development packages)." msgstr "pip wird dann pygobject und pycairo downloaden und compilieren. Dafür werden Gtk und cairo Header-Dateien (oder die Entwickler-Pakete) benötigt." #: msgid "(none, left, right, top, bottom, or after)." msgstr "(keine, links, rechts, oben, unten oder hinter)" #: msgid "_After slide pages" msgstr "" #: msgid "_Bottom half of slide" msgstr "_Untere Hälfte der Folie" #: msgid "_Left half of slide" msgstr "_Linke Seite der Folie" #: msgid "_Right half of slide" msgstr "_Rechte Hälfte der Folie" #: msgid "_Top half of slide" msgstr "_Obere Hälfte der Folie" #: msgid "Error parsing option from config file {}.{} \"{}\" to bool" msgstr "" #: msgid "Error parsing option from config file {}.{} \"{}\" to float" msgstr "" #: msgid "Error parsing option from config file {}.{} \"{}\" to int" msgstr "" #: msgid "Blank/unblank content screen" msgstr "" #: msgid "Close opened pympress instance" msgstr "" #: msgid "Print version and exit" msgstr "" #: msgid "Reset talk timer" msgstr "" #: msgid "Toggle pause of talk timer" msgstr "" #: msgid "GdkPixbuf gif player" msgstr "" #: msgid "Media support loaded: " msgstr "" #: msgid "Media support using {} is disabled." msgstr "" #: msgid "Media support: " msgstr "" #: msgid "Align _content" msgstr "" #: msgid "Clear and restore per page label" msgstr "" #: msgid "Clear and restore per page number" msgstr "" #: msgid "Clear on page change" msgstr "" #: msgid "Highlight mode" msgstr "" #: msgid "Never clear (manually only)" msgstr "" #: msgid "Tools below slide" msgstr "" #: msgid "Tools next to slide" msgstr "" #: msgid "_Close" msgstr "" #: msgid "_Open" msgstr "" #: msgid "_Pointer" msgstr "" #: msgid "_Quit" msgstr "" #: msgid "Gtk.Application.inhibit failed preventing screensaver, trying hard disabling" msgstr "" #: msgid "Should not require hard enable/disable screensaver on Linux" msgstr "" #: msgid "10th next slide" msgstr "" #: msgid "11th next slide" msgstr "" #: msgid "12th next slide" msgstr "" #: msgid "13th next slide" msgstr "" #: msgid "14th next slide" msgstr "" #: msgid "15th next slide" msgstr "" #: msgid "16th next slide" msgstr "" #: msgid "2nd next slide" msgstr "" #: msgid "3rd next slide" msgstr "" #: msgid "4th next slide" msgstr "" #: msgid "5th next slide" msgstr "" #: msgid "6th next slide" msgstr "" #: msgid "7th next slide" msgstr "" #: msgid "8th next slide" msgstr "" #: msgid "9th next slide" msgstr "" #: msgid "Edit layout" msgstr "" #: msgid "Plain layout, without notes mode" msgstr "" #: msgid "Unknown widget \"{}\" to draw" msgstr "" #: msgid "annotations (hideable)" msgstr "" #: msgid "box" msgstr "" #: msgid "current slide" msgstr "" #: msgid "horizontal" msgstr "" #: msgid "next slide(s)" msgstr "" #: msgid "next slides count" msgstr "" #: msgid "orientation" msgstr "" #: msgid "resizeable" msgstr "" #: msgid "vertical" msgstr "" #: msgid "widget" msgstr "" #: msgid "Layout for beamer notes on second screen (no current slide preview in notes)" msgstr "" #: msgid "Layout for libreoffice notes on separate pages (with current slide preview in notes)" msgstr "" #: msgid "Plain layout, without note slides" msgstr "" #: msgid "Overwrite" msgstr "" #: msgid "Overwrite changes instead of reloading?" msgstr "" #: msgid "Reload" msgstr "" #: msgid "Save as..." msgstr "" #: msgid "Save changes before closing?" msgstr "" #: msgid "Saving changes will overwrite the changed file!" msgstr "" #: msgid "The open file was modified outside of pympress but you have made unsaved changes." msgstr "" #: msgid "Unsaved changes" msgstr "" #: msgid "Unsaved changes will be lost" msgstr "" #: msgid "Unsaved changes will be lost." msgstr "" #: msgid "_Discard" msgstr "" #: msgid "_Save" msgstr "" #: msgid "_Save as" msgstr "" #: msgid "annotations" msgstr "" #: msgid "Auto" msgstr "" #: msgid "Choose parameters for automatically playing slides" msgstr "" #: msgid "Loop" msgstr "" #: msgid "Save file" msgstr "" #: msgid "Save file as" msgstr "" #: msgid "Time per slide (s):" msgstr "" #: msgid "_Automatic navigation" msgstr "" #: msgid "Note pages" msgstr "" #: msgid "Plain" msgstr "" #: msgid "notes" msgstr "" #: msgid "Highlighting" msgstr "" #: msgid "Layout to draw on the current slide" msgstr "" #: msgid "highlighting" msgstr "" pympress-1.7.1/pympress/share/locale/es/000077500000000000000000000000001415371354200202415ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/es/LC_MESSAGES/000077500000000000000000000000001415371354200220265ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/es/LC_MESSAGES/pympress.po000066400000000000000000000456471415371354200242700ustar00rootroot00000000000000msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: POEditor.com\n" "Project-Id-Version: pympress\n" "Language: es\n" #: msgid "(and optionally seconds)" msgstr "(y segundos, opcionalmente)" #: msgid "(paused)" msgstr "(en pausa)" #: msgid "Adjust alignment of slides in projector screen" msgstr "Ajuste de la alineación de diapositivas en la pantalla del proyector" #: msgid "All files" msgstr "Todos los archivos" #: msgid "Annotations" msgstr "Anotaciones" #: msgid "Big buttons" msgstr "Botones grandes" #: msgid "Cairo can not draw gif" msgstr "Cairo no puede abrir el gif" #: msgid "Clock" msgstr "Reloj" #: msgid "Content and presenter window must not be on the same monitor if you start full screen!" msgstr "Atención: Las ventanas de contenido y presentación no pueden estar en la misma pantalla para comenzar en pantalla completa!" #: msgid "Content blanked" msgstr "Contenido ocultado" #: msgid "Content fullscreen" msgstr "Contenido a pantalla completa" #: msgid "Contributors:" msgstr "Colaboradores" #: msgid "Could not disable DPMS screen blanking: got status " msgstr "No se puede deshabilitar el apagado de pantalla con DPMS: se obtuvo estado " #: msgid "Could not enable DPMS screen blanking: got status " msgstr "No se pudo activar el apagado de pantalla con DPMS: se obtuvo estado " #: msgid "Could not find the file \"{}\"" msgstr "No se puede abrir el archivo \"{}\"" #: msgid "Could not set screensaver status: got status " msgstr "Atención: El estado del salvapantallas no se puede recuperar: " #: msgid "Current slide" msgstr "Diapositiva actual" #: msgid "ERROR: Gobject Introspections module is missing, make sure Gtk and pygobject are installed on your system." msgstr "ERROR: falta el paquete python Gobject Introspections, asegúrese que Gtk y pygobject son bien instalados en su sistema." #: msgid "Error loading icon for about window" msgstr "Error al cargar los iconos" #: msgid "Error opening the file \"{}\"" msgstr "No se puede abrir el archivo \"{}\"" #: msgid "For instructions, refer to https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" msgstr "Para instrucciones (en inglés), remítase a https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" #: msgid "GtkImage gif player" msgstr "Reproductor de gif GtkImage" #: msgid "Highlight" msgstr "Resaltar" #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject" msgstr "Si está utilizando virtualenv o anaconda, puede o permitir el acceso a los paquetes del sistema, o ejecutar: pip install pygobject" #: msgid "Invalid log level \"{}\", try one of {}" msgstr "Nivel de registro incorrecto \"{}\", seleccione una de las siguientes opciones {}" #: msgid "Invalid time (mm or mm:ss expected), got \"{}\"" msgstr "Tiempo no válido (mm o mm:ss de espera), obtenido \"{}\"" #: msgid "Media support uses {}." msgstr "Los medios se soportan con {}" #: msgid "Next slide" msgstr "Siguiente diapositiva" #: msgid "No action was defined for this link" msgstr "No se definió ninguna acción para este enlace" #: msgid "Not starting content or presenter window full screen because there is only one monitor" msgstr "Es imposible poner el contenido o la ventana de visualización en pantalla completa porque sólo hay un monitor." #: msgid "Notes" msgstr "Notas" #: msgid "Notes position" msgstr "Posición de las notas" #: msgid "Open _Recent" msgstr "Abierto _Recientemente" #: msgid "Open..." msgstr "Abrir..." #: msgid "Options:" msgstr "Opciones:" #: msgid "Overrides the detection from the file." msgstr "Anula la detección desde el archivo." #: msgid "PDF files" msgstr "Archivos PDF" #: msgid "Pause" msgstr "Pausa" #: msgid "Play" msgstr "Ejecutar" #: msgid "Pointer" msgstr "Puntero" #: msgid "Portable installation" msgstr "Instalación portátil" #: msgid "Presentation timing breakdown" msgstr "Desglose del tiempo de presentación" #: msgid "Presenter fullscreen" msgstr "Presentación a pantalla completa" #: msgid "Pympress Content" msgstr "Contenido de pympress" #: msgid "Pympress Presenter" msgstr "Presentador de Pympress" #: msgid "Pympress can not extract attached file" msgstr "Pympress no puede extraer archivos adjuntos" #: msgid "Pympress can not extract embedded media" msgstr "Pympress no puede extraer los medios incorporados" #: msgid "Pympress can not find file " msgstr "Pympress no puede encontrar el archivo " #: msgid "Pympress can not interpret annotation of type:" msgstr "Pympress no puede interpretar anotaciones del tipo: " #: msgid "Pympress does not recognize link type \"{}\"" msgstr "Pympress no reconoce enlaces de tipo \"{}\"" #: msgid "Pympress does not recognize link type \"{}\" to \"{}\"" msgstr "Pympress no reconoce enlaces de tipo \"{}\" hacia \"{}\"" #: msgid "Pympress does not yet support link type \"{}\"" msgstr "Pympress aún no soporta enlaces como \"{}\"" #: msgid "Pympress does not yet support link type \"{}\" to \"{}\"" msgstr "Pympress aún no soporta enlaces como \"{}\" hacia \"{}\"" #: msgid "Python version {}" msgstr "Versión de python {}" #: msgid "Resources are loaded from " msgstr "Los recursos se cargan desde " #: msgid "Set level of verbosity in log file:" msgstr "Nivel de detalle en los logs:" #: msgid "Set talk _Time" msgstr "Seleccionar el _Tiempo de duración de la presentación" #: msgid "Set the position of notes on the pdf page (none, left, right, top, or bottom)." msgstr "Establecer la posición de las notas en la página del pdf (ninguna, izquierda, derecha, arriba o abajo)." #: msgid "Slide number" msgstr "Número de diapositiva" #: msgid "Some preferences are saved in " msgstr "Algunas preferencias se guardan en" #: msgid "Stop" msgstr "Parar" #: msgid "The estimated (intended) talk time in minutes" msgstr "Duración estimada (prevista) de la presentación, en minutos" #: msgid "The log is written to " msgstr "El registro se encuentro en " #: msgid "This help" msgstr "Este es un mensaje de ayuda" #: msgid "Time elapsed" msgstr "Tiempo transcurrido" #: msgid "Time estimation" msgstr "Tiempo estimado" #: msgid "Timing breakdown" msgstr "Desglose del tiempo de presentación" #: msgid "Unexpected action in index \"{}\"" msgstr "Acción imprevista en el índice \"{}\"" #: msgid "Unknow widget {} to be fullscreened, aborting." msgstr "Elemento visual {} desconocido a pantalla completa, parando." #: msgid "Unsupported OS: can't enable/disable screensaver" msgstr "Sistema operativo no soportado: ningún salvapantallas se puede activar/desactivar." #: msgid "Usage: {} [options] " msgstr "Uso: {} [opciones] ." #: msgid "Video support using {} is disabled." msgstr "El soporte de vídeo con {} está desactivado." #: msgid "_Align content" msgstr "_Alinear contenido" #: msgid "_Annotations" msgstr "_Anotaciones" #: msgid "_Blank screen" msgstr "Ocultar pantalla" #: msgid "_Blue" msgstr "Azul" #: msgid "_Bottom" msgstr "A_bajo" #: msgid "_Disabled" msgstr "_Desactivado" #: msgid "_File" msgstr "_Fichero" #: msgid "_First" msgstr "Primero" #: msgid "_Fullscreen" msgstr "Pantalla completa" #: msgid "_Go to..." msgstr "_Ir a..." #: msgid "_Green" msgstr "Verde" #: msgid "_Help" msgstr "Ayuda" #: msgid "_Highlight" msgstr "Resaltar" #: msgid "_Jump to label" msgstr "Saltar a la etiqueta" #: msgid "_Last" msgstr "Último" #: msgid "_Left" msgstr "Derecha" #: msgid "_Manual" msgstr "_Manual" #: msgid "_Navigation" msgstr "_Navegación" #: msgid "_Next" msgstr "Siguiente" #: msgid "_Notes mode" msgstr "Modo _Notas" #: msgid "_Pause timer" msgstr "Temporizador en _pausa" #: msgid "_Permanent" msgstr "_Permanente" #: msgid "_Presentation" msgstr "_Presentación" #: msgid "_Previous" msgstr "_Precedente" #: msgid "_Red" msgstr "_Rojo" #: msgid "_Reset timer" msgstr "_Reinicializar el temporizador" #: msgid "_Right" msgstr "Izquierda" #: msgid "_Shortcuts" msgstr "Acce_sos directos" #: msgid "_Starting Configuration" msgstr "Configuración del inicio" #: msgid "_Swap screens" msgstr "Intercambio de pantallas" #: msgid "_Top" msgstr "Arriba" #: msgid "_Undo zoom" msgstr "Deshacer zoom" #: msgid "_Zoom in" msgstr "Hacer _zoom" #: msgid "access denied when trying to access screen saver settings in registry!" msgstr "acceso denegado al cambiar la configuración del protector de pantalla durante el registro !" #: msgid "column" msgstr "columna" #: msgid "duration" msgstr "duración" #: msgid "name" msgstr "nombre" #: msgid "no action defined for this link!" msgstr "ninguna acción definida para este enlace !" #: msgid "page label" msgstr "etiqueta de página" #: msgid "pip will then download and compile pygobject, for which you need the Gtk headers (or development package)." msgstr "pip descargará y compilara pygobject, por lo que se requieren las cabeceras (o los paquetes de desarollo) de Gtk" #: msgid "pympress is a little PDF reader written in Python using Poppler for PDF rendering and GTK for the GUI.\n" "" msgstr "pympress es un lector de PDF ligero escrito en Python, usando Poppler para mostrar PDF y GTK en la interfaz gráfica de usuario..\n" "" #: msgid "slide" msgstr "diapositiva" #: msgid "slide #" msgstr "diapositiva #" #: msgid "time" msgstr "tiempo" #: msgid "{}, {}, {}, {}, or {}" msgstr "{}, {}, {}, {}, o {}" #: msgid "Additional features" msgstr "Características adicionales" #: msgid "Blank screen" msgstr "Pantalla en blanco" #: msgid "Cancel goto/jump/highlighting/zooming" msgstr "Cancelar ir a/saltar/resaltar/hacer zoom" #: msgid "Close file" msgstr "Cerrar archivo" #: msgid "First slide" msgstr "Primera diapositiva" #: msgid "Go back in slide history" msgstr "Atrás en el historial de dispositivas" #: msgid "Go forward in slide history" msgstr "Adelante en el historial de dispositivas" #: msgid "Go to page number" msgstr "Ir al número de página" #: msgid "Jump to page label" msgstr "Saltar a la página con etiqueta" #: msgid "Last slide" msgstr "Última diapositiva" #: msgid "Manage files" msgstr "Administrar archivos" #: msgid "Navigating" msgstr "Navegando" #: msgid "Next slide with different label" msgstr "Siguiente diapositiva con etiqueta diferente" #: msgid "Open file" msgstr "Abrir archivo" #: msgid "Play/pause timer" msgstr "Temporizador de reproducción/pausa" #: msgid "Presentation" msgstr "Presentación" #: msgid "Previous slide" msgstr "Diapositiva anterior" #: msgid "Previous slide with different label" msgstr "Diapositiva anterior con etiqueta diferente" #: msgid "Quit" msgstr "Salir" #: msgid "Reset timer" msgstr "Restablecer el temporizador" #: msgid "Set estimated talk time" msgstr "Ajustar el tiempo de conversación estimado" #: msgid "Swap windows" msgstr "Intercambiar ventanas" #: msgid "Timers" msgstr "Temporizadores" #: msgid "Toggle annotations" msgstr "Alternar anotaciones" #: msgid "Toggle fullscreen" msgstr "Alternar pantalla completa" #: msgid "Toggle highlighting" msgstr "Alternar resaltado" #: msgid "Toggle laserpointer" msgstr "Alternar puntero" #: msgid "Toggle notes mode" msgstr "Alternar el modo de notas" #: msgid "Undo highlight stroke" msgstr "Deshacer trazo de resalta" #: msgid "Unzoom" msgstr "Deshacer zoom" #: msgid "Validate goto/jump destination" msgstr "Validar diapositiva de destinación" #: msgid "Windows" msgstr "Ventanas" #: msgid "Zoom" msgstr "Zoom" #: msgid "Unrecognized named destination: " msgstr "Destinación nombrada no reconocida: " #: msgid "Unsupported link clicked. " msgstr "Enlace no soportado. " #: msgid "Fullscreen Presentation running" msgstr "Presentación ejecutando en pantalla completa" #: msgid "Caused by " msgstr "Causado por " #: msgid "Building FileWatcher" msgstr "Inicializando FileWatcher" #: msgid "Missing dependency: python \"{}\" package" msgstr "Falta dependencia: paquete python \"{}\"" #: msgid "Monitoring of changes to reload files automatically is not available" msgstr "Monitorización de cambios para refrescar ficheros automáticamente está desactivado" #: msgid "ERROR: Gobject Introspections and/or pycairo module is missing, make sure Gtk, pygobject and pycairo are installed on your system." msgstr "ERROR: falta une de los paquetes python Gobject Introspections o pycairo, asegúrese que Gtk, pygobject y pycairo son bien instalados en su sistema." #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject pycairo" msgstr "Si está utilizando virtualenv o anaconda, puede o permitir el acceso a los paquetes del sistema, o ejecutar: pip install pygobject pycairo" #: msgid "pip will then download and compile pygobject, for which you need the Gtk and cairo headers (or development packages)." msgstr "pip descargará y compilara pygobject, por lo que se requieren las cabeceras (o los paquetes de desarollo) de Gtk" #: msgid "(none, left, right, top, or bottom)." msgstr "(ninguna [none], izquierda [left], derecha [right], arriba [top] o abajo [bottom])." #: msgid "If using a virtualenv or anaconda, you can also try allowing system site packages." msgstr "Si está utilizando virtualenv o anaconda, también puede permitir el acceso a los paquetes del sistema" #: msgid "Set the position of notes on the pdf page" msgstr "Establecer la posición de las notas en la página del pdf" #: msgid "Try your operating system’s package manager, or try running: pip install pygobject pycairo" msgstr "Intente buscar en el gestor de paquetes de su sistema operativo, o ejecutar: pip install pygobject pycairo" #: msgid "pip will then download and compile pygobject and pycairo, for which you need the Gtk and cairo headers (or development packages)." msgstr "pip descargará y compilara pygobject y pycairo, por lo que se requieren las cabeceras (o los paquetes de desarollo) de Gtk y cairo" #: msgid "(none, left, right, top, bottom, or after)." msgstr "(ninguna [none], izquierda [left], derecha [right], arriba [top], abajo [bottom] o detrás [after])." #: msgid "_After slide pages" msgstr "_Después de las dispositivas" #: msgid "_Bottom half of slide" msgstr "Parte _inferior de la dispositiva" #: msgid "_Left half of slide" msgstr "Parte _izquierda de la diapositiva" #: msgid "_Right half of slide" msgstr "Parte _derecha de la diapositiva" #: msgid "_Top half of slide" msgstr "Parte _superior de la diapositiva" #: msgid "Error parsing option from config file {}.{} \"{}\" to bool" msgstr "Error convirtiendo opción del fichero de configuración {}.{} \"{}\" en booleano" #: msgid "Error parsing option from config file {}.{} \"{}\" to float" msgstr "Error convirtiendo opción del fichero de configuración {}.{} \"{}\" en número" #: msgid "Error parsing option from config file {}.{} \"{}\" to int" msgstr "Error convirtiendo opción del fichero de configuración {}.{} \"{}\" en número entero" #: msgid "Blank/unblank content screen" msgstr "Ocultar/revelar pantalla" #: msgid "Close opened pympress instance" msgstr "Salir de la instancia abierta de pympress" #: msgid "Print version and exit" msgstr "Enseñar versión y salir" #: msgid "Reset talk timer" msgstr "Reiniciar temporizador" #: msgid "Toggle pause of talk timer" msgstr "Alternar pausa del temporizador" #: msgid "GdkPixbuf gif player" msgstr "Reproductor de gif GdkPixbuf" #: msgid "Media support loaded: " msgstr "Los medios se soportan con: " #: msgid "Media support using {} is disabled." msgstr "El soporte de medios con {} está desactivado." #: msgid "Media support: " msgstr "Soporte de medios: " #: msgid "Align _content" msgstr "Alinear _contenido" #: msgid "Clear and restore per page label" msgstr "Limpiar y restaurar por etiqueta de página" #: msgid "Clear and restore per page number" msgstr "Limpiar y restaurar por número de página" #: msgid "Clear on page change" msgstr "Limpiar al cambio de página" #: msgid "Highlight mode" msgstr "Modo de resaltado" #: msgid "Never clear (manually only)" msgstr "Limipiar manualmente" #: msgid "Tools below slide" msgstr "Herramientas debajo de la dispositiva" #: msgid "Tools next to slide" msgstr "Herramientas al lado de la dispositiva" #: msgid "_Close" msgstr "_Cerrar" #: msgid "_Open" msgstr "_Abrir" #: msgid "_Pointer" msgstr "_Puntero" #: msgid "_Quit" msgstr "_Salir" #: msgid "Gtk.Application.inhibit failed preventing screensaver, trying hard disabling" msgstr "" #: msgid "Should not require hard enable/disable screensaver on Linux" msgstr "" #: msgid "10th next slide" msgstr "" #: msgid "11th next slide" msgstr "" #: msgid "12th next slide" msgstr "" #: msgid "13th next slide" msgstr "" #: msgid "14th next slide" msgstr "" #: msgid "15th next slide" msgstr "" #: msgid "16th next slide" msgstr "" #: msgid "2nd next slide" msgstr "" #: msgid "3rd next slide" msgstr "" #: msgid "4th next slide" msgstr "" #: msgid "5th next slide" msgstr "" #: msgid "6th next slide" msgstr "" #: msgid "7th next slide" msgstr "" #: msgid "8th next slide" msgstr "" #: msgid "9th next slide" msgstr "" #: msgid "Edit layout" msgstr "" #: msgid "Plain layout, without notes mode" msgstr "" #: msgid "Unknown widget \"{}\" to draw" msgstr "" #: msgid "annotations (hideable)" msgstr "" #: msgid "box" msgstr "" #: msgid "current slide" msgstr "" #: msgid "horizontal" msgstr "" #: msgid "next slide(s)" msgstr "" #: msgid "next slides count" msgstr "" #: msgid "orientation" msgstr "" #: msgid "resizeable" msgstr "" #: msgid "vertical" msgstr "" #: msgid "widget" msgstr "" #: msgid "Layout for beamer notes on second screen (no current slide preview in notes)" msgstr "" #: msgid "Layout for libreoffice notes on separate pages (with current slide preview in notes)" msgstr "" #: msgid "Plain layout, without note slides" msgstr "" #: msgid "Overwrite" msgstr "" #: msgid "Overwrite changes instead of reloading?" msgstr "" #: msgid "Reload" msgstr "" #: msgid "Save as..." msgstr "" #: msgid "Save changes before closing?" msgstr "" #: msgid "Saving changes will overwrite the changed file!" msgstr "" #: msgid "The open file was modified outside of pympress but you have made unsaved changes." msgstr "" #: msgid "Unsaved changes" msgstr "" #: msgid "Unsaved changes will be lost" msgstr "" #: msgid "Unsaved changes will be lost." msgstr "" #: msgid "_Discard" msgstr "" #: msgid "_Save" msgstr "" #: msgid "_Save as" msgstr "" #: msgid "annotations" msgstr "" #: msgid "Auto" msgstr "" #: msgid "Choose parameters for automatically playing slides" msgstr "" #: msgid "Loop" msgstr "" #: msgid "Save file" msgstr "" #: msgid "Save file as" msgstr "" #: msgid "Time per slide (s):" msgstr "" #: msgid "_Automatic navigation" msgstr "" #: msgid "Note pages" msgstr "" #: msgid "Plain" msgstr "" #: msgid "notes" msgstr "" #: msgid "Highlighting" msgstr "" #: msgid "Layout to draw on the current slide" msgstr "" #: msgid "highlighting" msgstr "" pympress-1.7.1/pympress/share/locale/fr/000077500000000000000000000000001415371354200202415ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/fr/LC_MESSAGES/000077500000000000000000000000001415371354200220265ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/fr/LC_MESSAGES/pympress.po000066400000000000000000000510341415371354200242530ustar00rootroot00000000000000msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: POEditor.com\n" "Project-Id-Version: pympress\n" "Language: fr\n" #: msgid "(and optionally seconds)" msgstr "(et secondes, optionellement)" #: msgid "(paused)" msgstr "(en pause)" #: msgid "Adjust alignment of slides in projector screen" msgstr "Ajuster l'alignement des diapos sur l'écran du projecteur" #: msgid "All files" msgstr "Tous fichiers" #: msgid "Annotations" msgstr "Notes" #: msgid "Big buttons" msgstr "Gros boutons" #: msgid "Cairo can not draw gif" msgstr "Cairo ne peut pas afficher le gif" #: msgid "Clock" msgstr "Horloge" #: msgid "Content and presenter window must not be on the same monitor if you start full screen!" msgstr "Attention: La fenêtre de contenu et de présenteur ne peuvent être sur le même écran pour démarrer en plein écran !" #: msgid "Content blanked" msgstr "Contenu masqué" #: msgid "Content fullscreen" msgstr "Contenu en plein écran" #: msgid "Contributors:" msgstr "Contributeurs" #: msgid "Could not disable DPMS screen blanking: got status " msgstr "Attention: Impossible de désactiver l'extinction d'écran DPMS: " #: msgid "Could not enable DPMS screen blanking: got status " msgstr "Attention: Impossible d'activer l'extinction d'écran DPMS: " #: msgid "Could not find the file \"{}\"" msgstr "Impossible d'ouvrir le fichier \"{}\"" #: msgid "Could not set screensaver status: got status " msgstr "Attention: Impossible de récupérer l'état de l'économiseur d'écran: " #: msgid "Current slide" msgstr "Diapo actuelle" #: msgid "ERROR: Gobject Introspections module is missing, make sure Gtk and pygobject are installed on your system." msgstr "ERREUR: le module Gobject Introspections est manquant, assurez vous que les dépendances Gtk et pygobject sont installées sur votre système." #: msgid "Error loading icon for about window" msgstr "Erreur de chargement des icônes" #: msgid "Error opening the file \"{}\"" msgstr "Impossible d'ouvrir le fichier \"{}\"" #: msgid "For instructions, refer to https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" msgstr "Des instructions sont disponibles (en anglais) à https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" #: msgid "GtkImage gif player" msgstr "lecteur de gif GtkImage" #: msgid "Highlight" msgstr "Feutre" #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject" msgstr "Si vous utilisez virtualenv ou anaconda, vous pouvez autoriser l'accès aux modules du système, ou lancer: pip install pygobject" #: msgid "Invalid log level \"{}\", try one of {}" msgstr "Mauvais niveau de log \"{}\", choisissez l'un de {}" #: msgid "Invalid time (mm or mm:ss expected), got \"{}\"" msgstr "Templs invalude (mm ou mm:ss attendu), \"{}\" reçu" #: msgid "Media support uses {}." msgstr "Les vidéos sont prises en charge par {}" #: msgid "Next slide" msgstr "Prochaine diapo" #: msgid "No action was defined for this link" msgstr "Pas d'action définie pour ce lien" #: msgid "Not starting content or presenter window full screen because there is only one monitor" msgstr "Impossible de mettre la fenêtre contenu ou présenteur en plein écran car il n'y a qu'un moniteur" #: msgid "Notes" msgstr "Notes" #: msgid "Notes position" msgstr "Position des notes" #: msgid "Open _Recent" msgstr "_Récemment ouverts" #: msgid "Open..." msgstr "Ouvrir..." #: msgid "Options:" msgstr "Otions:" #: msgid "Overrides the detection from the file." msgstr "Ce paramètre l'emporte sur la détection automatique des notes." #: msgid "PDF files" msgstr "Fichiers PDF" #: msgid "Pause" msgstr "Pause" #: msgid "Play" msgstr "Lire" #: msgid "Pointer" msgstr "Pointeur" #: msgid "Portable installation" msgstr "Installation portative" #: msgid "Presentation timing breakdown" msgstr "Analyse de la durée de la présentation" #: msgid "Presenter fullscreen" msgstr "Présenteur en plein écran" #: msgid "Pympress Content" msgstr "Pympress Contenu" #: msgid "Pympress Presenter" msgstr "Pympress Présenteur" #: msgid "Pympress can not extract attached file" msgstr "Pympress ne peut extraire le média intégré" #: msgid "Pympress can not extract embedded media" msgstr "Pympress ne peut extraire le média intégré" #: msgid "Pympress can not find file " msgstr "Pympress ne peut pas trouver le fichier " #: msgid "Pympress can not interpret annotation of type:" msgstr "Pympress ne peut pas interpréter d'annotation de type: " #: msgid "Pympress does not recognize link type \"{}\"" msgstr "Pympress ne reconnaît pas les liens de type \"{}\"" #: msgid "Pympress does not recognize link type \"{}\" to \"{}\"" msgstr "Pympress ne reconnaît pas les liens de type \"{}\" vers \"{}\"" #: msgid "Pympress does not yet support link type \"{}\"" msgstr "Pympress ne prend pas encore en charge les liens de type \"{}\"" #: msgid "Pympress does not yet support link type \"{}\" to \"{}\"" msgstr "Pympress ne prend pas encore en charge les liens de type \"{}\" vers \"{}\"" #: msgid "Python version {}" msgstr "Python version {}" #: msgid "Resources are loaded from " msgstr "Ressources chargées depuis " #: msgid "Set level of verbosity in log file:" msgstr "Niveau de détails dans les logs:" #: msgid "Set talk _Time" msgstr "Paramé_trer la durée de la présentation" #: msgid "Set the position of notes on the pdf page (none, left, right, top, or bottom)." msgstr "Disposition des notes dans le pdf (désactivé, gauche, droit, haut, ou bas)" #: msgid "Slide number" msgstr "Diapo numéro" #: msgid "Some preferences are saved in " msgstr "Certaines préférences sont sauvegardées dans" #: msgid "Stop" msgstr "Stop" #: msgid "The estimated (intended) talk time in minutes" msgstr "Durée estimée (prévue) de la présentation, en minutes" #: msgid "The log is written to " msgstr "Le journal est disponible à " #: msgid "This help" msgstr "Ce message d'aide" #: msgid "Time elapsed" msgstr "Temps écoulé" #: msgid "Time estimation" msgstr "Temps estimé" #: msgid "Timing breakdown" msgstr "Analyse du chronométrage" #: msgid "Unexpected action in index \"{}\"" msgstr "Action inattendue dans l'index \"{}\"" #: msgid "Unknow widget {} to be fullscreened, aborting." msgstr "Élement visuel {} inconnu à mettre en plein écran, abandonnée." #: msgid "Unsupported OS: can't enable/disable screensaver" msgstr "Système d'exploitation non pris en charge: impossible d'activer/désactiver l'économiseur d'écran" #: msgid "Usage: {} [options] " msgstr "Usage: {} [options] " #: msgid "Video support using {} is disabled." msgstr "La prise en charge vidéo grâce à {} est désactivée" #: msgid "_Align content" msgstr "_Aligner la diapo" #: msgid "_Annotations" msgstr "_Annotations" #: msgid "_Blank screen" msgstr "Masquer l'écran" #: msgid "_Blue" msgstr "_Bleu" #: msgid "_Bottom" msgstr "_Bas" #: msgid "_Disabled" msgstr "_Désactivé" #: msgid "_File" msgstr "_Fichier" #: msgid "_First" msgstr "_Premier" #: msgid "_Fullscreen" msgstr "Plein écran" #: msgid "_Go to..." msgstr "Aller à la diapo" #: msgid "_Green" msgstr "Vert" #: msgid "_Help" msgstr "Aide" #: msgid "_Highlight" msgstr "Feutre" #: msgid "_Jump to label" msgstr "Aller au numéro de page" #: msgid "_Last" msgstr "_Dernier" #: msgid "_Left" msgstr "Gauche" #: msgid "_Manual" msgstr "_Manuel" #: msgid "_Navigation" msgstr "_Navigation" #: msgid "_Next" msgstr "_Suivant" #: msgid "_Notes mode" msgstr "Mode _note" #: msgid "_Pause timer" msgstr "Mettre le minuteur en _pause" #: msgid "_Permanent" msgstr "_Permanent" #: msgid "_Presentation" msgstr "_Présentation" #: msgid "_Previous" msgstr "_Précédent" #: msgid "_Red" msgstr "_Rouge" #: msgid "_Reset timer" msgstr "_Réinitialier le minuteur" #: msgid "_Right" msgstr "D_roite" #: msgid "_Shortcuts" msgstr "Raccourci_s" #: msgid "_Starting Configuration" msgstr "Configuration de démarrage" #: msgid "_Swap screens" msgstr "Échanger les écran_s" #: msgid "_Top" msgstr "Hau_t" #: msgid "_Undo zoom" msgstr "Ann_uler le zoom" #: msgid "_Zoom in" msgstr "_Zoomer" #: msgid "access denied when trying to access screen saver settings in registry!" msgstr "accès refusé lors de la modification des paramètres d'économiseur d'écran dans le registre !" #: msgid "column" msgstr "colonne" #: msgid "duration" msgstr "durée" #: msgid "name" msgstr "nom" #: msgid "no action defined for this link!" msgstr "pas d'action définie pour ce lien !" #: msgid "page label" msgstr "numéro de page" #: msgid "pip will then download and compile pygobject, for which you need the Gtk headers (or development package)." msgstr "pip téléchargera et compilera pygobject, ce pour quoi vous aurez besoin des fichiers d'en-tête Gtk (ou du pacquet logiciel de développement)" #: msgid "pympress is a little PDF reader written in Python using Poppler for PDF rendering and GTK for the GUI.\n" "" msgstr "pympress est un lecteur de PDF léger écrit en Python, utilisant Poppler pour afficher les PDF et GTK pour l'interface graphique.\n" "" #: msgid "slide" msgstr "diapo" #: msgid "slide #" msgstr "diapo #" #: msgid "time" msgstr "temps" #: msgid "{}, {}, {}, {}, or {}" msgstr "{}, {}, {}, {}, ou {}" #: msgid "Additional features" msgstr "Fonctionnalités supplémentaires" #: msgid "Blank screen" msgstr "Masquer l'écran" #: msgid "Cancel goto/jump/highlighting/zooming" msgstr "Annuler modes aller à/feutre/zoom" #: msgid "Close file" msgstr "Fermer le fichier" #: msgid "First slide" msgstr "Première diapo" #: msgid "Go back in slide history" msgstr "Aller à la diapo précédente chronologiquement" #: msgid "Go forward in slide history" msgstr "Aller à la diapo suivante chronologiquement" #: msgid "Go to page number" msgstr "Aller à la diapo" #: msgid "Jump to page label" msgstr "Aller au numéro de page" #: msgid "Last slide" msgstr "Prochaine diapo" #: msgid "Manage files" msgstr "Gestion des fichiers" #: msgid "Navigating" msgstr "Navigation" #: msgid "Next slide with different label" msgstr "Prochaine diapo avec numéro de page différent" #: msgid "Open file" msgstr "Ouvrir le fichier" #: msgid "Play/pause timer" msgstr "Mettre le minuteur en pause" #: msgid "Presentation" msgstr "Présentation" #: msgid "Previous slide" msgstr "Diapo précédente" #: msgid "Previous slide with different label" msgstr "Précédente diapo avec numéro de page différent" #: msgid "Quit" msgstr "Quitter" #: msgid "Reset timer" msgstr "Réinitialier le minuteur" #: msgid "Set estimated talk time" msgstr "Paramétrer la durée de la présentation" #: msgid "Swap windows" msgstr "Intervertir les fenêtres" #: msgid "Timers" msgstr "Minuteurs" #: msgid "Toggle annotations" msgstr "Mode annotations" #: msgid "Toggle fullscreen" msgstr "Contenu en plein écran" #: msgid "Toggle highlighting" msgstr "Mode feutre" #: msgid "Toggle laserpointer" msgstr "Activer le pointeur laser" #: msgid "Toggle notes mode" msgstr "Mode note" #: msgid "Undo highlight stroke" msgstr "Annuler trait de feutre" #: msgid "Unzoom" msgstr "Annuler le zoom" #: msgid "Validate goto/jump destination" msgstr "Valider la destination" #: msgid "Windows" msgstr "Fenêtres" #: msgid "Zoom" msgstr "Zoomer" #: msgid "Unrecognized named destination: " msgstr "Nom de la destination non reconnu: " #: msgid "Unsupported link clicked. " msgstr "Lien non supporté activé. " #: msgid "Fullscreen Presentation running" msgstr "Présentation en plein écran en cours" #: msgid "Caused by " msgstr "Causé(e) par" #: msgid "Building FileWatcher" msgstr "Construction du FileWatcher" #: msgid "Missing dependency: python \"{}\" package" msgstr "Il manque une dépendance : le module python \"{}\" " #: msgid "Monitoring of changes to reload files automatically is not available" msgstr "Le contrôle des changements pour recharger automatiquement les fichiers n’est pas disponible" #: msgid "ERROR: Gobject Introspections and/or pycairo module is missing, make sure Gtk, pygobject and pycairo are installed on your system." msgstr "Erreur: le module Gobject Introspections et/ou le module pycairo sont/est manquant. Assurez-vous d’avoir installé Gtk, pygobject et pycairo sur votre système." #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject pycairo" msgstr "Si vous utilisez un environnement virtuel ou anaconda, vous pouvez soit autoriser les modules installé sur votre système, soit exécuter: pip install pygobject pycairo" #: msgid "pip will then download and compile pygobject, for which you need the Gtk and cairo headers (or development packages)." msgstr "pip téléchargera et compilera pygobject, pour lequel vous avez besoin des headers (ou modules de développement) Gtk et cairo." #: msgid "(none, left, right, top, or bottom)." msgstr "(aucune, gauche, droite, haut ou bas)" #: msgid "If using a virtualenv or anaconda, you can also try allowing system site packages." msgstr "Si vous utilisez un environnement virtuel ou anaconda, vous pouvez aussi essayer de permettre l’utilisation des modules installés sur votre système." #: msgid "Set the position of notes on the pdf page" msgstr "Définissez la position des notes sur la page pdf." #: msgid "Try your operating system’s package manager, or try running: pip install pygobject pycairo" msgstr "Vous pouvez chercher dans le gestionnaire de paquets de votre système d’exploitation, ou essayer: pip install pyobject pycairo" #: msgid "pip will then download and compile pygobject and pycairo, for which you need the Gtk and cairo headers (or development packages)." msgstr "pip téléchargera et compilera pygobject et pycairo, ce pour quoi il vous faudra les fichiers d’en-têtes (ou les paquets de développement) de Gtk et cairo" #: msgid "(none, left, right, top, bottom, or after)." msgstr "(aucun [none], left [gauche], droite [right], haut [top], bas [bottom], ou après [after])" #: msgid "_After slide pages" msgstr "_Après les diapos" #: msgid "_Bottom half of slide" msgstr "_Bas des diapos" #: msgid "_Left half of slide" msgstr "_Gauche des diapos" #: msgid "_Right half of slide" msgstr "_Droite des diapos" #: msgid "_Top half of slide" msgstr "_Haut des diaops" #: msgid "Error parsing option from config file {}.{} \"{}\" to bool" msgstr "Erreur de conversion d’une option dans le fichier de configuration {}.{} en booléen: \"{}\"" #: msgid "Error parsing option from config file {}.{} \"{}\" to float" msgstr "Erreur de conversion d’une option dans le fichier de configuration {}.{} en nombre flottant: \"{}\"" #: msgid "Error parsing option from config file {}.{} \"{}\" to int" msgstr "Erreur de conversion d’une option dans le fichier de configuration {}.{} en nombre entier: \"{}\"" #: msgid "Blank/unblank content screen" msgstr "Masquer/dé l’écran" #: msgid "Close opened pympress instance" msgstr "Fermer l’instance de pympress ouverte" #: msgid "Print version and exit" msgstr "Afficher la version et quitter" #: msgid "Reset talk timer" msgstr "Réinitialiser le timer" #: msgid "Toggle pause of talk timer" msgstr "Mettre en pause ou reprendre le timer" #: msgid "GdkPixbuf gif player" msgstr "Lecteur de gif Gdkixbuf" #: msgid "Media support loaded: " msgstr "Médias supportés: " #: msgid "Media support using {} is disabled." msgstr "Le support média {} est désactivé" #: msgid "Media support: " msgstr "Médias supportés: " #: msgid "Align _content" msgstr "Aligner la diapo" #: msgid "Clear and restore per page label" msgstr "Effacer et restaurer par diapo" #: msgid "Clear and restore per page number" msgstr "Effacer et restaurer par numéro de page" #: msgid "Clear on page change" msgstr "Effacer en changeant de diapo" #: msgid "Highlight mode" msgstr "Mode feutre" #: msgid "Never clear (manually only)" msgstr "Effacer manuellement" #: msgid "Tools below slide" msgstr "Outils sous la diapo" #: msgid "Tools next to slide" msgstr "Outils à droite de la diapo" #: msgid "_Close" msgstr "_Fermer" #: msgid "_Open" msgstr "_Ouvrir" #: msgid "_Pointer" msgstr "_Pointeur" #: msgid "_Quit" msgstr "_Quitter" #: msgid "Gtk.Application.inhibit failed preventing screensaver, trying hard disabling" msgstr "Gtk.Application.inhibit n’a pas réussi à désactiver l’économiseur d’écran, recours à la méthode forte" #: msgid "Should not require hard enable/disable screensaver on Linux" msgstr "La méthode forte de désactivation d’économiseur d’écran ne devrait pas être nécessaire sur Linux" #: msgid "10th next slide" msgstr "10ème diapo suivante" #: msgid "11th next slide" msgstr "11ème diapo suivante" #: msgid "12th next slide" msgstr "12ème diapo suivante" #: msgid "13th next slide" msgstr "13ème diapo suivante" #: msgid "14th next slide" msgstr "14ème diapo suivante" #: msgid "15th next slide" msgstr "15ème diapo suivante" #: msgid "16th next slide" msgstr "16ème diapo suivante" #: msgid "2nd next slide" msgstr "2ème diapo suivante" #: msgid "3rd next slide" msgstr "3ème diapo suivante" #: msgid "4th next slide" msgstr "4ème diapo suivante" #: msgid "5th next slide" msgstr "5ème diapo suivante" #: msgid "6th next slide" msgstr "6ème diapo suivante" #: msgid "7th next slide" msgstr "7ème diapo suivante" #: msgid "8th next slide" msgstr "8ème diapo suivante" #: msgid "9th next slide" msgstr "9ème diapo suivante" #: msgid "Edit layout" msgstr "Modifier la mise en page" #: msgid "Plain layout, without notes mode" msgstr "Mise en page normale, sans diapos de notes" #: msgid "Unknown widget \"{}\" to draw" msgstr "Élément visuel à afficher \"{}\" inconnu" #: msgid "annotations (hideable)" msgstr "annotations (masquable)" #: msgid "box" msgstr "conteneur" #: msgid "current slide" msgstr "diapo actuelle" #: msgid "horizontal" msgstr "horizontal" #: msgid "next slide(s)" msgstr "diapo(s) suivante(s)" #: msgid "next slides count" msgstr "nombre de diapos suivantes" #: msgid "orientation" msgstr "orientation" #: msgid "resizeable" msgstr "redimensionnable" #: msgid "vertical" msgstr "vertical" #: msgid "widget" msgstr "élément visuel" #: msgid "Layout for beamer notes on second screen (no current slide preview in notes)" msgstr "Mise en page pour diapo de notes beamer sur la même page (sans aperçu de la diapo actuelle dans les notes)" #: msgid "Layout for libreoffice notes on separate pages (with current slide preview in notes)" msgstr "Mise en page pour diapo de notes libreoffice sur page séparée (avec aperçu de la diapo actuelle dans les notes)" #: msgid "Plain layout, without note slides" msgstr "Mise en page sans diapos de notes" #: msgid "Overwrite" msgstr "Écraser" #: msgid "Overwrite changes instead of reloading?" msgstr "Écraser les modifications au lieu de recharger?" #: msgid "Reload" msgstr "Recharger" #: msgid "Save as..." msgstr "Enregistrer sous…" #: msgid "Save changes before closing?" msgstr "Sauvegarder les changements avant de fermer?" #: msgid "Saving changes will overwrite the changed file!" msgstr "Sauvegarder les changements écrasera le fichier modifié!" #: msgid "The open file was modified outside of pympress but you have made unsaved changes." msgstr "Le fichier ouvert a été modifié en dehors de pympress, mais vous avez fait des modifications non sauvegardées." #: msgid "Unsaved changes" msgstr "Modifications non sauvegardées" #: msgid "Unsaved changes will be lost" msgstr "Les modifications non sauvegardées seront perdues" #: msgid "Unsaved changes will be lost." msgstr "Les modifications non sauvegardées seront perdues." #: msgid "_Discard" msgstr "Ignorer" #: msgid "_Save" msgstr "Enregistrer" #: msgid "_Save as" msgstr "Enregistrer sous" #: msgid "annotations" msgstr "annotations" #: msgid "Auto" msgstr "Auto" #: msgid "Choose parameters for automatically playing slides" msgstr "Choisissez les paramètre pour passer les diapos automatiquement" #: msgid "Loop" msgstr "Boucler" #: msgid "Save file" msgstr "Sauvegarder le fichier" #: msgid "Save file as" msgstr "Sauvegarder le fichier sous…" #: msgid "Time per slide (s):" msgstr "Temps par slide (s)" #: msgid "_Automatic navigation" msgstr "Passage automatique" #: msgid "Note pages" msgstr "Diapo de notes" #: msgid "Plain" msgstr "Sans notes" #: msgid "notes" msgstr "" #: msgid "Highlighting" msgstr "" #: msgid "Layout to draw on the current slide" msgstr "" #: msgid "highlighting" msgstr "" pympress-1.7.1/pympress/share/locale/pl/000077500000000000000000000000001415371354200202455ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/pl/LC_MESSAGES/000077500000000000000000000000001415371354200220325ustar00rootroot00000000000000pympress-1.7.1/pympress/share/locale/pl/LC_MESSAGES/pympress.po000066400000000000000000000412571415371354200242650ustar00rootroot00000000000000msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: POEditor.com\n" "Project-Id-Version: pympress\n" "Language: pl\n" #: msgid "(and optionally seconds)" msgstr "(oraz opcjonalnie w sekundach)" #: msgid "(paused)" msgstr "(wstrzymany)" #: msgid "Adjust alignment of slides in projector screen" msgstr "Wyreguluj wyrównanie slajdów na ekranie rzutnika" #: msgid "All files" msgstr "Wszystkie pliki" #: msgid "Annotations" msgstr "Adnotacje" #: msgid "Big buttons" msgstr "Duże przyciski" #: msgid "Cairo can not draw gif" msgstr "Cairo nie może narysować gif-a" #: msgid "Clock" msgstr "Zegar" #: msgid "Content and presenter window must not be on the same monitor if you start full screen!" msgstr "Okno treści oraz okno prezentera nie mogą znajdować się na tym samym monitorze, gdy uruchomisz tryb pełnego ekranu!" #: msgid "Content blanked" msgstr "Puste okno treści" #: msgid "Content fullscreen" msgstr "Okno treści w trybie pełnego ekranu" #: msgid "Contributors:" msgstr "Współpracownicy:" #: msgid "Could not disable DPMS screen blanking: got status " msgstr "nie można wyłączyć wygaszania ekranu DPMS: otrzymany status " #: msgid "Could not enable DPMS screen blanking: got status " msgstr "nie można włączyć wygaszania ekranu DPMS: otrzymany status " #: msgid "Could not find the file \"{}\"" msgstr "Nie mogę znaleźć pliku \"{}\"" #: msgid "Could not set screensaver status: got status " msgstr "nie można ustawić stanu wygaszacza ekranu: otrzymany status " #: msgid "Current slide" msgstr "Bieżący slajd" #: msgid "ERROR: Gobject Introspections module is missing, make sure Gtk and pygobject are installed on your system." msgstr "BŁĄD: brakuje modułu Gobject Introspections, upewnij się, że Gtk i pyobject są poprawnie zainstalowane w systemie." #: msgid "Error loading icon for about window" msgstr "Błąd ładowania ikony dla okna \"O\"" #: msgid "Error opening the file \"{}\"" msgstr "Błąd podczas otwierania pliku \"{}\"" #: msgid "For instructions, refer to https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" msgstr "Instrukcje: https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" #: msgid "GtkImage gif player" msgstr "Odtwarzacz gifa GtkImage" #: msgid "Highlight" msgstr "Zakreślacz" #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject" msgstr "Jeśli używasz vrtualenv lub anacondy, możesz zezwolić na pakiety systemowe albo uruchomić: pip install pygobject" #: msgid "Invalid log level \"{}\", try one of {}" msgstr "Nieprawidłowy poziom dziennikowania \"{}\", wypróbuj jeden z {}" #: msgid "Invalid time (mm or mm:ss expected), got \"{}\"" msgstr "Nieprawidłowy czas (oczekiwano min lub min:sek), podano (wprowadzono) \"{}\"" #: msgid "Media support uses {}." msgstr "Obsługa multimediów używa {}" #: msgid "Next slide" msgstr "Następny slajd" #: msgid "No action was defined for this link" msgstr "Dla tego odnośnika nie zdefiniowano żadnych działań" #: msgid "Not starting content or presenter window full screen because there is only one monitor" msgstr "Nie uruchomiono okna treści lub okna prezentera w trybie pełnego ekranu, ponieważ jest tylko jeden monitor" #: msgid "Notes" msgstr "Notatki" #: msgid "Notes position" msgstr "Pozycja notatek" #: msgid "Open _Recent" msgstr "Otwórz _Ostatni" #: msgid "Open..." msgstr "Otwórz..." #: msgid "Options:" msgstr "Opcje:" #: msgid "Overrides the detection from the file." msgstr "Nadpisz wykrywanie z pliku." #: msgid "PDF files" msgstr "Pliki PDF" #: msgid "Pause" msgstr "Wstrzymaj" #: msgid "Play" msgstr "Odtwarzaj" #: msgid "Pointer" msgstr "Wskaźnik" #: msgid "Portable installation" msgstr "Intalacja przenośna" #: msgid "Presentation timing breakdown" msgstr "" #: msgid "Presenter fullscreen" msgstr "Okno prelegenta w trybie pełnego ekranu" #: msgid "Pympress Content" msgstr "Okno treści Pympress" #: msgid "Pympress Presenter" msgstr "Okno presentera Pympress" #: msgid "Pympress can not extract attached file" msgstr "Pympress nie może wyodrębnić załączonego pliku" #: msgid "Pympress can not extract embedded media" msgstr "Pympress nie może wyodrębnić osadzonych mediów" #: msgid "Pympress can not find file " msgstr "Pympress nie może znaleźć pliku " #: msgid "Pympress can not interpret annotation of type:" msgstr "Pympress nie może zinterpretować adnotacji typu:" #: msgid "Pympress does not recognize link type \"{}\"" msgstr "Pympress nie rozpoznaje odsyłacza typu \"{}\"" #: msgid "Pympress does not recognize link type \"{}\" to \"{}\"" msgstr "Pympress nie rozpoznaje odsyłacza typu \"{}\" do \"{}\"" #: msgid "Pympress does not yet support link type \"{}\"" msgstr "Pympress jeszcze nie obsługuje odsyłacza typu \"{}\"" #: msgid "Pympress does not yet support link type \"{}\" to \"{}\"" msgstr "Pympress jeszcze nie obsługuje odsyłacza typu \"{}\" do \"{}\"" #: msgid "Python version {}" msgstr "Python wersja {}" #: msgid "Resources are loaded from " msgstr "Zasoby są ładowane z " #: msgid "Set level of verbosity in log file:" msgstr "Ustaw poziom szczegółowości pliku dziennika:" #: msgid "Set talk _Time" msgstr "Ustaw _czas trwania wystąpienia (prelekcji)" #: msgid "Set the position of notes on the pdf page (none, left, right, top, or bottom)." msgstr "Ustaw pozycję notatek na stronie pdf (brak, lewo, prawo, góra lub dół)." #: msgid "Slide number" msgstr "Numer slajdu" #: msgid "Some preferences are saved in " msgstr "Niektóre ustawienia są zapisywane w " #: msgid "Stop" msgstr "Zatrzymaj" #: msgid "The estimated (intended) talk time in minutes" msgstr "Przewidywany (spodziewany) czas trwania wystąpienia (prelekcji) w minutach" #: msgid "The log is written to " msgstr "Logi są zapisywane do " #: msgid "This help" msgstr "Niniejsza pomoc" #: msgid "Time elapsed" msgstr "Czas, który upłynął" #: msgid "Time estimation" msgstr "Przewidziany czas" #: msgid "Timing breakdown" msgstr "" #: #, fuzzy msgid "Unexpected action in index \"{}\"" msgstr "Niespodziewana akcja w indeksie \"{}\"" #: msgid "Unknow widget {} to be fullscreened, aborting." msgstr "Nie mogę uruchomić w trybie pełnego ekranu, nieznanego widżetu {}, przerywam działanie." #: msgid "Unsupported OS: can't enable/disable screensaver" msgstr "Nieobsługiwany system operacyjny: nie można włączyć/wyłączyć wygaszacza ekranu" #: msgid "Usage: {} [options] " msgstr "Sposób użycia: {} [opcje] " #: msgid "Video support using {} is disabled." msgstr "Obsługa wideo przy użyciu {} jest wyłączona." #: msgid "_Align content" msgstr "_Wyrównaj treść" #: msgid "_Annotations" msgstr "_Adnotacje" #: msgid "_Blank screen" msgstr "_Pusty ekran" #: msgid "_Blue" msgstr "_Niebieski" #: msgid "_Bottom" msgstr "Dół" #: msgid "_Disabled" msgstr "_Nieaktywny" #: msgid "_File" msgstr "_Plik" #: msgid "_First" msgstr "_Pierwszy" #: msgid "_Fullscreen" msgstr "_Tryb pełnego ekranu" #: msgid "_Go to..." msgstr "_Idź do..." #: msgid "_Green" msgstr "_Zielony" #: msgid "_Help" msgstr "_Pomoc" #: msgid "_Highlight" msgstr "_Zakreślacz" #: msgid "_Jump to label" msgstr "Skocz do etykiety" #: msgid "_Last" msgstr "_Ostatni" #: msgid "_Left" msgstr "_Lewo" #: msgid "_Manual" msgstr "_Ręczny" #: msgid "_Navigation" msgstr "_Nawigacja" #: msgid "_Next" msgstr "_Następny" #: msgid "_Notes mode" msgstr "_Tryb notatek" #: msgid "_Pause timer" msgstr "_Wstrzymaj timer" #: msgid "_Permanent" msgstr "_Trwały" #: msgid "_Presentation" msgstr "_Prezentacja" #: msgid "_Previous" msgstr "_Poprzedni" #: msgid "_Red" msgstr "_Czerwony" #: msgid "_Reset timer" msgstr "Zresetuj timer" #: msgid "_Right" msgstr "P_rawo" #: msgid "_Shortcuts" msgstr "_Skróty" #: msgid "_Starting Configuration" msgstr "_Konfiguracja startowa" #: msgid "_Swap screens" msgstr "_Zamień ekrany" #: msgid "_Top" msgstr "Góra" #: msgid "_Undo zoom" msgstr "Cofnij zbliżenie" #: msgid "_Zoom in" msgstr "_Zbliżenie" #: msgid "access denied when trying to access screen saver settings in registry!" msgstr "odmowa dostępu przy próbie uzyskania dostępu do ustawień wygaszacza ekranu w rejestrze!" #: msgid "column" msgstr "kolumna" #: msgid "duration" msgstr "czas trwania" #: msgid "name" msgstr "nazwa" #: msgid "no action defined for this link!" msgstr "nie określono działania dla tego odnośnika!" #: msgid "page label" msgstr "etykieta strony" #: #, fuzzy msgid "pip will then download and compile pygobject, for which you need the Gtk headers (or development package)." msgstr "pip ściągnie i skompiluje pyobject, do którego wymagane są nagłówki Gtk (albo pakiet developerski)" #: msgid "pympress is a little PDF reader written in Python using Poppler for PDF rendering and GTK for the GUI.\n" "" msgstr "pympress jest małym czytnikiem PDF napisanym w Pythonie, używającym Popplera do renderowania plików PDF oraz GTK do tworzenia GUI.\n" "" #: msgid "slide" msgstr "slajd" #: msgid "slide #" msgstr "slajd #" #: msgid "time" msgstr "czas" #: msgid "{}, {}, {}, {}, or {}" msgstr "{}, {}, {}, {} lub {}" #: msgid "Additional features" msgstr "Dodatkowe funkcje" #: msgid "Blank screen" msgstr "Pusty ekran" #: msgid "Cancel goto/jump/highlighting/zooming" msgstr "Anuluj idź do/skocz/zakreślanie/zbliżanie" #: msgid "Close file" msgstr "Zamknij plik" #: msgid "First slide" msgstr "Pierwszy slajd" #: msgid "Go back in slide history" msgstr "Idź wstecz w historii slajdów" #: msgid "Go forward in slide history" msgstr "Idź dalej w historii slajdów" #: msgid "Go to page number" msgstr "Idź do strony o numerze" #: msgid "Jump to page label" msgstr "Skocz do strony o etykiecie" #: msgid "Last slide" msgstr "Ostatni slajd" #: msgid "Manage files" msgstr "Zarządzaj plikami" #: msgid "Navigating" msgstr "Nawigowanie" #: msgid "Next slide with different label" msgstr "Następny slajd" #: msgid "Open file" msgstr "Otwórz plik" #: msgid "Play/pause timer" msgstr "Uruchom / wstrzymaj licznik czasu" #: msgid "Presentation" msgstr "Prezentacja" #: msgid "Previous slide" msgstr "Poprzedni slajd" #: msgid "Previous slide with different label" msgstr "Poprzedni slajd" #: msgid "Quit" msgstr "Wyjdź" #: msgid "Reset timer" msgstr "Zresetuj licznik czasu" #: msgid "Set estimated talk time" msgstr "Ustaw szacowany czas trwania prelekcji" #: msgid "Swap windows" msgstr "Zamień okna" #: msgid "Timers" msgstr "Liczniki czasu" #: msgid "Toggle annotations" msgstr "Przełącz adnotacje" #: msgid "Toggle fullscreen" msgstr "Przełącz tryb pełnoekranowy" #: msgid "Toggle highlighting" msgstr "Przełącz zakreślanie" #: #, fuzzy msgid "Toggle laserpointer" msgstr "Włącz/wyłącz wskaźnik laserowy" #: msgid "Toggle notes mode" msgstr "Przełącz tryb notatek" #: msgid "Undo highlight stroke" msgstr "Cofnij wyróżnienie obrysu" #: msgid "Unzoom" msgstr "Cofnij powiększanie" #: msgid "Validate goto/jump destination" msgstr "" #: msgid "Windows" msgstr "Okna" #: msgid "Zoom" msgstr "Powiększanie" #: msgid "Unrecognized named destination: " msgstr "" #: msgid "Unsupported link clicked. " msgstr "" #: msgid "Fullscreen Presentation running" msgstr "" #: msgid "Caused by " msgstr "Spowodowane przez " #: #, fuzzy msgid "Building FileWatcher" msgstr "Budowanie obserwatora plików (FileWatcher)" #: #, fuzzy msgid "Missing dependency: python \"{}\" package" msgstr "Brakuje zależności: python \"{}\" package" #: msgid "Monitoring of changes to reload files automatically is not available" msgstr "" #: msgid "ERROR: Gobject Introspections and/or pycairo module is missing, make sure Gtk, pygobject and pycairo are installed on your system." msgstr "" #: msgid "If using a virtualenv or anaconda, you can either allow system site packages, or run: pip install pygobject pycairo" msgstr "" #: msgid "pip will then download and compile pygobject, for which you need the Gtk and cairo headers (or development packages)." msgstr "" #: msgid "(none, left, right, top, or bottom)." msgstr "" #: msgid "If using a virtualenv or anaconda, you can also try allowing system site packages." msgstr "" #: msgid "Set the position of notes on the pdf page" msgstr "Ustaw pozycję notatek na stronie pdf" #: msgid "Try your operating system’s package manager, or try running: pip install pygobject pycairo" msgstr "" #: msgid "pip will then download and compile pygobject and pycairo, for which you need the Gtk and cairo headers (or development packages)." msgstr "" #: msgid "(none, left, right, top, bottom, or after)." msgstr "" #: msgid "_After slide pages" msgstr "" #: msgid "_Bottom half of slide" msgstr "" #: msgid "_Left half of slide" msgstr "" #: msgid "_Right half of slide" msgstr "" #: msgid "_Top half of slide" msgstr "" #: msgid "Error parsing option from config file {}.{} \"{}\" to bool" msgstr "" #: msgid "Error parsing option from config file {}.{} \"{}\" to float" msgstr "" #: msgid "Error parsing option from config file {}.{} \"{}\" to int" msgstr "" #: msgid "Blank/unblank content screen" msgstr "" #: msgid "Close opened pympress instance" msgstr "" #: msgid "Print version and exit" msgstr "" #: msgid "Reset talk timer" msgstr "" #: msgid "Toggle pause of talk timer" msgstr "" #: msgid "GdkPixbuf gif player" msgstr "" #: msgid "Media support loaded: " msgstr "" #: msgid "Media support using {} is disabled." msgstr "" #: msgid "Media support: " msgstr "" #: msgid "Align _content" msgstr "" #: msgid "Clear and restore per page label" msgstr "" #: msgid "Clear and restore per page number" msgstr "" #: msgid "Clear on page change" msgstr "" #: msgid "Highlight mode" msgstr "" #: msgid "Never clear (manually only)" msgstr "" #: msgid "Tools below slide" msgstr "" #: msgid "Tools next to slide" msgstr "" #: msgid "_Close" msgstr "" #: msgid "_Open" msgstr "" #: msgid "_Pointer" msgstr "" #: msgid "_Quit" msgstr "" #: msgid "Gtk.Application.inhibit failed preventing screensaver, trying hard disabling" msgstr "" #: msgid "Should not require hard enable/disable screensaver on Linux" msgstr "" #: msgid "10th next slide" msgstr "" #: msgid "11th next slide" msgstr "" #: msgid "12th next slide" msgstr "" #: msgid "13th next slide" msgstr "" #: msgid "14th next slide" msgstr "" #: msgid "15th next slide" msgstr "" #: msgid "16th next slide" msgstr "" #: msgid "2nd next slide" msgstr "" #: msgid "3rd next slide" msgstr "" #: msgid "4th next slide" msgstr "" #: msgid "5th next slide" msgstr "" #: msgid "6th next slide" msgstr "" #: msgid "7th next slide" msgstr "" #: msgid "8th next slide" msgstr "" #: msgid "9th next slide" msgstr "" #: msgid "Edit layout" msgstr "" #: msgid "Plain layout, without notes mode" msgstr "" #: msgid "Unknown widget \"{}\" to draw" msgstr "" #: msgid "annotations (hideable)" msgstr "" #: msgid "box" msgstr "" #: msgid "current slide" msgstr "" #: msgid "horizontal" msgstr "" #: msgid "next slide(s)" msgstr "" #: msgid "next slides count" msgstr "" #: msgid "orientation" msgstr "" #: msgid "resizeable" msgstr "" #: msgid "vertical" msgstr "" #: msgid "widget" msgstr "" #: msgid "Layout for beamer notes on second screen (no current slide preview in notes)" msgstr "" #: msgid "Layout for libreoffice notes on separate pages (with current slide preview in notes)" msgstr "" #: msgid "Plain layout, without note slides" msgstr "" #: msgid "Overwrite" msgstr "" #: msgid "Overwrite changes instead of reloading?" msgstr "" #: msgid "Reload" msgstr "" #: msgid "Save as..." msgstr "" #: msgid "Save changes before closing?" msgstr "" #: msgid "Saving changes will overwrite the changed file!" msgstr "" #: msgid "The open file was modified outside of pympress but you have made unsaved changes." msgstr "" #: msgid "Unsaved changes" msgstr "" #: msgid "Unsaved changes will be lost" msgstr "" #: msgid "Unsaved changes will be lost." msgstr "" #: msgid "_Discard" msgstr "" #: msgid "_Save" msgstr "" #: msgid "_Save as" msgstr "" #: msgid "annotations" msgstr "" #: msgid "Auto" msgstr "" #: msgid "Choose parameters for automatically playing slides" msgstr "" #: msgid "Loop" msgstr "" #: msgid "Save file" msgstr "" #: msgid "Save file as" msgstr "" #: msgid "Time per slide (s):" msgstr "" #: msgid "_Automatic navigation" msgstr "" #: msgid "Note pages" msgstr "" #: msgid "Plain" msgstr "" #: msgid "notes" msgstr "" #: msgid "Highlighting" msgstr "" #: msgid "Layout to draw on the current slide" msgstr "" #: msgid "highlighting" msgstr "" pympress-1.7.1/pympress/share/locale/pympress.pot000066400000000000000000000230561415371354200222460ustar00rootroot00000000000000msgid "(and optionally seconds)" msgstr "" msgid "(none, left, right, top, bottom, or after)." msgstr "" msgid "(paused)" msgstr "" msgid "10th next slide" msgstr "" msgid "11th next slide" msgstr "" msgid "12th next slide" msgstr "" msgid "13th next slide" msgstr "" msgid "14th next slide" msgstr "" msgid "15th next slide" msgstr "" msgid "16th next slide" msgstr "" msgid "2nd next slide" msgstr "" msgid "3rd next slide" msgstr "" msgid "4th next slide" msgstr "" msgid "5th next slide" msgstr "" msgid "6th next slide" msgstr "" msgid "7th next slide" msgstr "" msgid "8th next slide" msgstr "" msgid "9th next slide" msgstr "" msgid "Additional features" msgstr "" msgid "Adjust alignment of slides in projector screen" msgstr "" msgid "Align _content" msgstr "" msgid "All files" msgstr "" msgid "Annotations" msgstr "" msgid "Auto" msgstr "" msgid "Big buttons" msgstr "" msgid "Blank screen" msgstr "" msgid "Blank/unblank content screen" msgstr "" msgid "Cairo can not draw gif" msgstr "" msgid "Cancel goto/jump/highlighting/zooming" msgstr "" msgid "Caused by " msgstr "" msgid "Choose parameters for automatically playing slides" msgstr "" msgid "Clear and restore per page label" msgstr "" msgid "Clear and restore per page number" msgstr "" msgid "Clear on page change" msgstr "" msgid "Clock" msgstr "" msgid "Close file" msgstr "" msgid "Close opened pympress instance" msgstr "" msgid "Content and presenter window must not be on the same monitor if you start full screen!" msgstr "" msgid "Content blanked" msgstr "" msgid "Content fullscreen" msgstr "" msgid "Contributors:" msgstr "" msgid "Could not find the file \"{}\"" msgstr "" msgid "Current slide" msgstr "" msgid "ERROR: Gobject Introspections and/or pycairo module is missing, make sure Gtk, pygobject and pycairo are installed on your system." msgstr "" msgid "Edit layout" msgstr "" msgid "Error loading icon for about window" msgstr "" msgid "Error opening the file \"{}\"" msgstr "" msgid "Error parsing option from config file {}.{} \"{}\" to bool" msgstr "" msgid "Error parsing option from config file {}.{} \"{}\" to float" msgstr "" msgid "Error parsing option from config file {}.{} \"{}\" to int" msgstr "" msgid "First slide" msgstr "" msgid "For instructions, refer to https://github.com/Cimbali/pympress/blob/master/README.md#dependencies" msgstr "" msgid "Fullscreen Presentation running" msgstr "" msgid "GdkPixbuf gif player" msgstr "" msgid "Go back in slide history" msgstr "" msgid "Go forward in slide history" msgstr "" msgid "Go to page number" msgstr "" msgid "Gtk.Application.inhibit failed preventing screensaver, trying hard disabling" msgstr "" msgid "Highlight" msgstr "" msgid "Highlight mode" msgstr "" msgid "If using a virtualenv or anaconda, you can also try allowing system site packages." msgstr "" msgid "Invalid log level \"{}\", try one of {}" msgstr "" msgid "Invalid time (mm or mm:ss expected), got \"{}\"" msgstr "" msgid "Jump to page label" msgstr "" msgid "Last slide" msgstr "" msgid "Layout for beamer notes on second screen (no current slide preview in notes)" msgstr "" msgid "Layout for libreoffice notes on separate pages (with current slide preview in notes)" msgstr "" msgid "Loop" msgstr "" msgid "Manage files" msgstr "" msgid "Media support uses {}." msgstr "" msgid "Media support using {} is disabled." msgstr "" msgid "Media support: " msgstr "" msgid "Missing dependency: python \"{}\" package" msgstr "" msgid "Monitoring of changes to reload files automatically is not available" msgstr "" msgid "Navigating" msgstr "" msgid "Never clear (manually only)" msgstr "" msgid "Next slide" msgstr "" msgid "Next slide with different label" msgstr "" msgid "Not starting content or presenter window full screen because there is only one monitor" msgstr "" msgid "Note pages" msgstr "" msgid "Notes" msgstr "" msgid "Notes position" msgstr "" msgid "Open _Recent" msgstr "" msgid "Open file" msgstr "" msgid "Open..." msgstr "" msgid "Overrides the detection from the file." msgstr "" msgid "Overwrite" msgstr "" msgid "Overwrite changes instead of reloading?" msgstr "" msgid "PDF files" msgstr "" msgid "Pause" msgstr "" msgid "Plain" msgstr "" msgid "Plain layout, without note slides" msgstr "" msgid "Play" msgstr "" msgid "Play/pause timer" msgstr "" msgid "Pointer" msgstr "" msgid "Portable installation" msgstr "" msgid "Presentation" msgstr "" msgid "Presentation timing breakdown" msgstr "" msgid "Presenter fullscreen" msgstr "" msgid "Previous slide" msgstr "" msgid "Previous slide with different label" msgstr "" msgid "Print version and exit" msgstr "" msgid "Pympress Content" msgstr "" msgid "Pympress Presenter" msgstr "" msgid "Pympress can not extract attached file" msgstr "" msgid "Pympress can not extract embedded media" msgstr "" msgid "Pympress can not find file " msgstr "" msgid "Pympress can not interpret annotation of type:" msgstr "" msgid "Pympress does not recognize link type \"{}\"" msgstr "" msgid "Pympress does not recognize link type \"{}\" to \"{}\"" msgstr "" msgid "Pympress does not yet support link type \"{}\"" msgstr "" msgid "Pympress does not yet support link type \"{}\" to \"{}\"" msgstr "" msgid "Python version {}" msgstr "" msgid "Quit" msgstr "" msgid "Reload" msgstr "" msgid "Reset talk timer" msgstr "" msgid "Reset timer" msgstr "" msgid "Resources are loaded from " msgstr "" msgid "Save as..." msgstr "" msgid "Save changes before closing?" msgstr "" msgid "Save file" msgstr "" msgid "Save file as" msgstr "" msgid "Saving changes will overwrite the changed file!" msgstr "" msgid "Set estimated talk time" msgstr "" msgid "Set level of verbosity in log file:" msgstr "" msgid "Set talk _Time" msgstr "" msgid "Set the position of notes on the pdf page" msgstr "" msgid "Should not require hard enable/disable screensaver on Linux" msgstr "" msgid "Slide number" msgstr "" msgid "Some preferences are saved in " msgstr "" msgid "Stop" msgstr "" msgid "Swap windows" msgstr "" msgid "The estimated (intended) talk time in minutes" msgstr "" msgid "The log is written to " msgstr "" msgid "The open file was modified outside of pympress but you have made unsaved changes." msgstr "" msgid "Time elapsed" msgstr "" msgid "Time estimation" msgstr "" msgid "Time per slide (s):" msgstr "" msgid "Timers" msgstr "" msgid "Timing breakdown" msgstr "" msgid "Toggle annotations" msgstr "" msgid "Toggle fullscreen" msgstr "" msgid "Toggle highlighting" msgstr "" msgid "Toggle laserpointer" msgstr "" msgid "Toggle notes mode" msgstr "" msgid "Toggle pause of talk timer" msgstr "" msgid "Tools below slide" msgstr "" msgid "Tools next to slide" msgstr "" msgid "Try your operating system’s package manager, or try running: pip install pygobject pycairo" msgstr "" msgid "Undo highlight stroke" msgstr "" msgid "Unexpected action in index \"{}\"" msgstr "" msgid "Unknow widget {} to be fullscreened, aborting." msgstr "" msgid "Unknown widget \"{}\" to draw" msgstr "" msgid "Unrecognized named destination: " msgstr "" msgid "Unsaved changes" msgstr "" msgid "Unsaved changes will be lost" msgstr "" msgid "Unsaved changes will be lost." msgstr "" msgid "Unsupported OS: can't enable/disable screensaver" msgstr "" msgid "Unsupported link clicked. " msgstr "" msgid "Unzoom" msgstr "" msgid "Validate goto/jump destination" msgstr "" msgid "Windows" msgstr "" msgid "Zoom" msgstr "" msgid "_After slide pages" msgstr "" msgid "_Annotations" msgstr "" msgid "_Automatic navigation" msgstr "" msgid "_Blank screen" msgstr "" msgid "_Blue" msgstr "" msgid "_Bottom half of slide" msgstr "" msgid "_Close" msgstr "" msgid "_Disabled" msgstr "" msgid "_Discard" msgstr "" msgid "_File" msgstr "" msgid "_First" msgstr "" msgid "_Fullscreen" msgstr "" msgid "_Go to..." msgstr "" msgid "_Green" msgstr "" msgid "_Help" msgstr "" msgid "_Highlight" msgstr "" msgid "_Jump to label" msgstr "" msgid "_Last" msgstr "" msgid "_Left half of slide" msgstr "" msgid "_Manual" msgstr "" msgid "_Navigation" msgstr "" msgid "_Next" msgstr "" msgid "_Notes mode" msgstr "" msgid "_Open" msgstr "" msgid "_Pause timer" msgstr "" msgid "_Permanent" msgstr "" msgid "_Pointer" msgstr "" msgid "_Presentation" msgstr "" msgid "_Previous" msgstr "" msgid "_Quit" msgstr "" msgid "_Red" msgstr "" msgid "_Reset timer" msgstr "" msgid "_Right half of slide" msgstr "" msgid "_Save" msgstr "" msgid "_Save as" msgstr "" msgid "_Shortcuts" msgstr "" msgid "_Starting Configuration" msgstr "" msgid "_Swap screens" msgstr "" msgid "_Top half of slide" msgstr "" msgid "_Undo zoom" msgstr "" msgid "_Zoom in" msgstr "" msgid "access denied when trying to access screen saver settings in registry!" msgstr "" msgid "annotations" msgstr "" msgid "annotations (hideable)" msgstr "" msgid "box" msgstr "" msgid "current slide" msgstr "" msgid "duration" msgstr "" msgid "horizontal" msgstr "" msgid "name" msgstr "" msgid "next slide(s)" msgstr "" msgid "next slides count" msgstr "" msgid "no action defined for this link!" msgstr "" msgid "notes" msgstr "" msgid "orientation" msgstr "" msgid "page label" msgstr "" msgid "pip will then download and compile pygobject and pycairo, for which you need the Gtk and cairo headers (or development packages)." msgstr "" msgid "pympress is a little PDF reader written in Python using Poppler for PDF rendering and GTK for the GUI.\n" msgstr "" msgid "resizeable" msgstr "" msgid "slide" msgstr "" msgid "slide #" msgstr "" msgid "time" msgstr "" msgid "vertical" msgstr "" msgid "widget" msgstr "" msgid "{}, {}, {}, {}, or {}" msgstr "" pympress-1.7.1/pympress/share/pixmaps/000077500000000000000000000000001415371354200200545ustar00rootroot00000000000000pympress-1.7.1/pympress/share/pixmaps/eraser.png000066400000000000000000000056151415371354200220520ustar00rootroot00000000000000PNG  IHDR>a pHYsAtEXtSoftwarewww.inkscape.org< IDATxlUg?N)6W1CR%%Dd1fq3@Qk-QX ,P&b k-z{c=O<=޾yB2X 4qx$`4PZt!a{-]cKLr{-]h;ԑe2 qMoC!`o^=%;-P3[ {DHfh8koyw̷ûTB囸k `I+$0W2t;^$25Q ޚoi)Lc,C|[>r! |@K\8 D]*K;3o"l;.|o%Ea/\|ml::BKQEnwFha/AFD&^"%H_#8'+gl@anhE *ˁq喾\|&)XTDІ|P3e xPDo W1e!K9KP #>e-}G)SWEʳw9Y7VU%򋬺brn ~Pz `~Б]X? I]S z ~BY;/: M*e=j.GqnRW(S:f<}3rn}y`iiiG<61[xB`e;676nxU4}g٢iѱf/*yuTLLZy***h4Yf!;̓+;D65#߾ʕ+WKJJD R3*C0@CCCŧs@Qd^HZߴi˿p…nAnis״Co۶EiNQ/<} K)wsK nxCCClӜT$qL}(+D0xBiNxl6ae;"pkk)٦9MCCCT9,)`ΝIJPӴ'Omڵ0\5Tyg{${^٦9͖-[]/Z/ A.^8 4Yvm(T1^R J~&QS(O3N$UUU͢|l<PVV懦tu]E1@ 0bTTTV&&&&͛gwz4NDH|9eK,i5 p4M"< G)>y+Vc)KgrN %իW76/[vq)!CEFmmo ZZZ:ktģP͛7mf]}Ѝ+X}wΝ;65UbX \6)xq|KtR/))*Knݺf؏QL?}O*ʽ{9e&u s$&P̙3lzSB^FpK0222ZTT%E&u"l*,,aE IENDB`pympress-1.7.1/pympress/share/pixmaps/make-png000077500000000000000000000001541415371354200215010ustar00rootroot00000000000000#!/bin/sh for SIZE in 16 22 24 32 48 64 128; do inkscape -w $SIZE -e pympress-$SIZE.png pympress.svg done pympress-1.7.1/pympress/share/pixmaps/marker_1.png000066400000000000000000000045461415371354200222740ustar00rootroot00000000000000PNG  IHDR>a pHYs)[tEXtSoftwarewww.inkscape.org<IDATx{T]bi]-AvA+hEŖXhjmZKcjcm h1ZMQA$)](T93sǹ|p w|d>xhHt$9X i ,?ы[sOXN88HyA,EP*3xh'Q@_B`b[c9;%V Q+?0{=)nS* p0xx f[[|``-Iŭui#wrq4~(,_ H]QqBTm5)(TTp{QƽqwHaGmENڅ69>)U2OwE.2s_'J&(/`tMp7؎TR2#{+6?%)6N~"z=S*}̂=[df9sǛ6ēWR'2 U630kW!r"bӀ^Q^dHI~jbCuQDkPRBcÁ݆QD:OOc8␟~1} .td1o<Ѿ:OǺb! 㓀|m;|%KȨ<.Ȭ0umL%FT,dV%g;bbkrKLKBG)p &XKJKk. ,d2x#xȭ.(2TR` B6!MQQ J CI}Oq8?.*a职AS _xF{Wz̉Q^`dbQB1x8OD9(2}sG4w'%,Anw@fFmm'".7"OLCVRM$_kA!W {A䫆/b^jȾ@܀LE\ *+6xH½2VC#_@t:Dd0| "Šg4!!Kx Q-gQ֮?:Q~ "eVrH!?_N:k /= |$o`rdgD ThkbI!1F,[*51y}38$?Qe22 ٩3,!:T~YZ}C5 q˪X!:;aiBg`Rg;,M6T~GDA&iҌCD CQCJl6{9V1;w<[i3r-*?c n GgfZ2wS]H~;-$T\dS6rCgTV`wr)1ûl3c4*cnBFpϟuHoPv *?CF${"XWLT=Hv'"s'kCgFZxhR2BKFw|On{T~ v}.)Cю \] T\$jΥ=e&dZʜ Vځs $kl-Y#B$&e9#uո%kLAgbj ":ejӰ]SnIp/S4c!.r|)ulIZ"9|[Qw cPg^~s|@g> ,WE.ˑia!"v]i:Y(lWzI K"5)ШUa pHYsGNtEXtSoftwarewww.inkscape.org< rIDATxi]Eo4H,D0D1eQ*hDP1Be(>%Zbs0XJPW%"(! dL']߽fDp?Dnߋ "?Y'%.I>K`VP4 Ƨd0XKP W9m2HP[{Ȟӏ_}9mI^DR<*`"A:-DfX A l! m&#;u] 9ɔ\}Hv'OR}ȶo%/]Γ Ƌ1%w*2PE!PZa"paܴQ$,V?bY }*yI9[/Ч'cD,Vb*'j$@̓ ]73> Ex06밯$u tCqK;L\M~faqu\lGǐc$0mvĿN|_s [1Y\uz k1bk;~0^yG\8\2jt^0a߇+dvvНh`qއuDlן{8 ؋︤; Z3gcߍxX}mX-EGG,_,B~#]$\E+Xq!~7CHu/%aZv#K.BjDoO%cď\҉0ew_M"ۿI ]%1.>c=CJ A$VO>|CJ*l_<4щ쏏Dx?7oCJױXхRCJc02"^9 #5TR lļw0bӁ]z)ts9@ʧG}݊20nl;X_AJ|'$swߎ:r%ԑu9v}"CMTY$Q- èmTlg̢j2{1LA)Jus5e J'cZ?=3uoAs-ŏ"eQuF)Ǚ{r,o#WCR'AحV)Z)&z@Ǩ8"^(!+F(dŖz@?Q߈֏J.3Y-mԲd}TVڀӐ\h]$~MeҋT|)RDmobIENDB`pympress-1.7.1/pympress/share/pixmaps/marker_3.png000066400000000000000000000070351415371354200222720ustar00rootroot00000000000000PNG  IHDR>a pHYscc^tEXtSoftwarewww.inkscape.org< IDATx{pK0$@rx(Qi:ZhmvRZ5e|'FBh(SBI0߮r1ww=3wwv56;O!e?v60.,-c*LOnoC(^ɭu-x]Mt`o? GeYvK9 XB{Dy?gCpKBWs@Q ;ՙ '[x?Щ8 hέKm . Li5 vD+:UQ (5%@&/vgp>; oi'_1p|ܧ,XZI`,>Bg]} / Dl{ă[m;~>D1t]!V@? *8KߊDžHYj?)T)GWZ`HxŪ%n+_ O6s0 6 D"K_`_'ZqH_`pJrEЕ4ʼn[ѷvѧMt yɛ*"DI^$䛘:XE!^HDFu&*erݥz~l-I܀O٢(#?ubӔh'h;+'Z5z&)T(EHt}CBEB$ㆦ2 EOx$FN5/p(ڧwCB+RG2[]ާ/)^BWDA7S[?ѕ?-D.ԄhZhYw(W\n.LD'tAlǙtԇ*J8=~0zD(/.@TޯUD7,g"Oyj>E,bpu2ql/]P?K*JLFW_on S(1=T54E)⽞ ͟d1o֟ea{@ C2}[ \Rj Wj߶CC+2Dlq%}|.C7_dnB]PQ(`TX#Lܭ=Ps0*D۽'=z>8{1!C~[D˥B !gXGOdѷC2tEO0,D"AeS[BRHj@@qYƝ8%`l'wr f?+?~|^`k`B'oCLBWsߦp.%rB1yzʑ@^3k!08'o7[wou'7ܜG[|-D"q;up>@05؃[H2qg8z'*zG|d;C!Ư.$e kZQ'{;20]O QqڷO~!Oz/t!Y {Ծ|e O$^'ħ@~=Ċ&iYB/O/B[О\L$CCb!2 A݌C8M(W$q$ H#`+4\ mF,^ e^0z" 1PI(4T!1}Ow?O{WP qjEXzd WGD*v^8r'n;k֬d1֭K]Q7y#C'WOm-HJ}krvp@_ccog!kZ O'Wo{{xN̺3* OU}ڍl.B>QW4 7K aܹsWcLmm.k^Rc/ːݻw?`ر ˃C*hU5|v }I}`hpvŊko\HǞO2mIn ' ʖ;X~]EfiiOL_*;\]˾Yf}諪ةS'*Rdq^qvϛ7c6nܸsΚ!Ư$7r#pf1{쥅h+%lڻW[Ŀ֚ KL߇xz<~}[=g)]#Vx&CqHdZ6Sf31D7IInmKLq$ ?D]oiYђA%;}~3D\(BB{nOIg%-=80yRr?Kq299d M:K$rLA><dAX,-9̼Q'IENDB`pympress-1.7.1/pympress/share/pixmaps/marker_fill_1.png000066400000000000000000000014151415371354200232720ustar00rootroot00000000000000PNG  IHDR>a pHYs`atEXtSoftwarewww.inkscape.org<IDATxMMaNfԘ >bX5Y KIDXHJ(Rl||.(Zbjy;>zzL$I$I$I$I$I$I$I$I$I$I:k;zM`hsTp,~~N }y&q RU&X9[=~~ATgo*bcU/1~Fcʵ A0~Wx1ƪ\c o.S:IENDB`pympress-1.7.1/pympress/share/pixmaps/marker_fill_2.png000066400000000000000000000016421415371354200232750ustar00rootroot00000000000000PNG  IHDR>a pHYs`atEXtSoftwarewww.inkscape.org</IDATx͋uƔ/aPd *.\HiUm¥"Ҧ$&0t FE0I1 F6-Pws{3a].6b].4}].+⢴iԒş5- x"k {SRf? <5-S੬Tn)pgR bIjf9ջxdq|ׯ'jf%pTnSTKy HlRU JG-xY#Q P}gFؐ5ʭ.w`8k [LliլTn= o۲RWƿ 5m$>=`$k \#6άTnY# H^Tn+9e r7?Hޤz k !>T-rHF/ukOn I||/ukh^{RvS}KCN|/ukh/9ԭǟ/ K.S9FK.Ԯ%Rw s]/u{HKn*]/u{`ܱSٖK>xۇ/}l;vB$I$I$I$I$I$I_ #AIENDB`pympress-1.7.1/pympress/share/pixmaps/marker_fill_3.png000066400000000000000000000027671415371354200233070ustar00rootroot00000000000000PNG  IHDR>a pHYs`atEXtSoftwarewww.inkscape.org<IDATxhVUr9]-&/(!HDQ 9B0A?D) (]EA?( ̂ji\sgϦϞ{~zr\W7sAWHFѿ3lγ7xaI`%6oo̷RaJ ';V~}@JP+1Z8׳,y20'z+ρC XIy^Hf5ܧX7MRZ(z6d:-Kx>O>h.2ؕ_'Ը*c]z7KoGjKc;y5!K(?H`Sg/8KΊ3c_lLCw/6Keq!M5LC.ɳ.5k?aK͚mhG0gG|KE3̉2)sGŽO8|31i/xsOHLC̛.!og/3o- /3oޖ?c_f ,Ɲ""a_fioxHZK, $c_bT ⎌/h9K,`eYID x}roYID ؈}>ق^}<ľ@N >+þ@=)}r <$}sCII4z>+m~.'h~!ˀ߱/''&qXUȒӅA Nc\ 0n-40,XG Msn '&CY盭ϽiØ&{ϽZ;j=dDork!EUs3M\-gM6`_o'&/7#%[//DֆPݳdcIr苞.~<]-ĭ.7/r;| mȭwӵuy@w |}yy[;nϛuyy xn|6WE?%鹅ɣ[Eمʏb%!Pۻv|:pṘ[NuNVFY?7u)v?w+"M**RԗQ~?z_I f%T~'u[20oF 8\&?oJg_WS6CEǯO nH%Spnd)%N]&.>I);|ݷv:יHJ[qk<1҆Tb:p F+QnHZpۦkFpIj&vp\R^%2A' wc؃6eJ"> 0t4bfe=/LFJ;IENDB`pympress-1.7.1/pympress/share/pixmaps/pointer.svg000066400000000000000000000066121415371354200222620ustar00rootroot00000000000000 image/svg+xml pympress-1.7.1/pympress/share/pixmaps/pointer_blue.png000066400000000000000000000024021415371354200232470ustar00rootroot00000000000000PNG  IHDR;0sBIT|d pHYs   \tEXtSoftwarewww.inkscape.org<IDATHnSݤD#2 Yz6c E&,XddY8@ँ^tz{v9+\&S byL|.}5 xnhl ;P˅8H 6nsE/ Cl69V鉚OVgRpkkGBęX0V N ANb-([f:3\>t cSEC[ f6UZ%3Eѩh$;zR \v#LT.ߍ3I!= 9ߕO0P%`)[6Î:q0Tk &L{D0Q_.[9 {ֳڄ }Ք1NEBT4MnE+B0$cj` "gy熥e%ݝ9,HXZEsYLZJTYznn@4(-3}k3[rTzj=cpt[u NBT;98 J Xvmc.}=#IYYoym(\LOWR/U9(t"YY?)~oW%퍾GdP0[hjSJ-j`hD28[K4/ꭹPGb!zAmrJP窴L#lt?=qF?=u':kM)FVqkHBws+ϝ`kקJO7έέ_qvWm, 6њJZQ+k%`-Z riѾΜCq;Sl%R-&T.DڮVvw)YzCޚuGwLߩqlY0V]0Smϥ7+ǢKB.J]eQYQEQc(|.&R]L:4Tή"N=*:2Jar]M"徸xVp+"JA7;\%d$5$Wi Nj;ꔳ.T8~2Imʙ.,f3T=ڛO?3Wd+uA!?`uRipi(zw '?h}R /ooomO/~a9:}ҤIENDB`pympress-1.7.1/pympress/share/pixmaps/pointer_green.png000066400000000000000000000017201415371354200234220ustar00rootroot00000000000000PNG  IHDR;0sBIT|d pHYs   \tEXtSoftwarewww.inkscape.org<MIDATHKnAƃ`LHl)&' ^$&!0 ewSi0Jwׄ#~e5lM$+;K4s=[`K`#3: Kx <88X%mi+ `a `p|z_Ψr}Lo':R];-7y`rzdV .3}{U>(me)uT0e*!~[Fu9޸2[5RJkPDb/]>8[rhGe+av-jGGpLhg{7)k^m3)\ -` LJmԖ܀W粆TL'N~`Ĩ~ To:Ʃ@=]w/|<`2sM]ہe@F@3}g>A-^~[]k5(eo,[Jd#FŐ䁇!ɈQAULl_Ϟme#P:&|a'R-]R~hwPmR4($ԶQf*KwH/.75p.9|,(TϚ;i|vso+9kTaplF-c}뗄2?/%Qc?iܷũԆ}sMXϏ 3e_:Nn])O}| n>X L~{ϰg <ޔ@FUB{[6{ ~+ Չ+̛$ԬZ*IENDB`pympress-1.7.1/pympress/share/pixmaps/pointer_red.png000066400000000000000000000017151415371354200231000ustar00rootroot00000000000000PNG  IHDR;0sBIT|d pHYs   \tEXtSoftwarewww.inkscape.org<JIDATHR"A ADdժxy, 0:ab3*n-U))ɗ?Iwg?ՀXM~PXW0\msr[K`>;w \Usmvn!7UpGMeVkgh T\߀/K)Rǹ| JoE_]Qms`%jS` =[]G+wY8 .n-\6RxOT[5:ZӜ n>6\"PGHު! fF ]˚%ex5 cbWn 8q{Qk3Lvf.ju][f&ryL~E}۫c8i"ܒڜ:@y_sBĚb( ]Q:&w  !J-W9 _5HPӠw<>RAQ#tZ]Lk@Z5A]&a#e~k@MjWۊ i4qh4iӚhxQ6g6eEMm؇(}#\)۲vE<T2O MS QۨG2N&7ʋ_( { csʍoo=S>$%!`l)SsgU&C`y{xY5lC dBy(tcRycp=+_"NޯK!#uV/-20*cQt 'nc0cZ5lZ[~;iiÀ?ӇMdXbzJۚI]SWHqg/^#ܼZnoôI BNV:x(PݤUHdXѩeX~+tڃk\c-󶪒%{TaUz1'3kO$?1H- 0 )Ÿ]gx PDT|*~(I&qk6z! Ra=?C4?O#UDt\Ř}.=EL[Syg;OX$@XyZTn]1Z7*IA@ؓ|*KOzt&}wIIENDB`pympress-1.7.1/pympress/share/pixmaps/pympress-22.png000066400000000000000000000023651415371354200226730ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYsaa0UtEXtSoftwarewww.inkscape.org<rIDAT8{LU?s$ t) -kLqlh-66MxkN-,RCԡVb]"|9{yy͔r6Ru b_LzptJ_Q]}H)ڔP2iV*׃@U? |5okAy󵲖bzC 3H(.[X o {i6$k㚗壈T\p,ADHs ֚OZiiWN֣rQ~CTĎ]ʪ 2^D>%݌++"P*MQە1W_%N2hhB+ƅ=y &+od?0A|@QcȱB,+hIFg\,um)LH&VCƓ~ ޥb[ݾfFB +<_"^h$&KCv+;@vz ,YtW~3.]{i *mw6%gz}^NfT\`pV/H%?߼2^t]fK.<8&,7IENDB`pympress-1.7.1/pympress/share/pixmaps/pympress-24.png000066400000000000000000000025711415371354200226740ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYs6GtEXtSoftwarewww.inkscape.org<IDATHYlUU}Q4E)`!AeT#`(Ac P$DD@r b g>goභ dd_{﵅R6!"$& Lv*5R1K&1" ޗ-Dh*Qk},4& jUB08LHU )8/N¯.J!XHf?R >U=sUgY87֋6BH(Up[k0M+R.fqii?)WG5~'SJQ9H:`jW0*ԏ(u*ԱQUQ[kJrBſN-"21lzXLMV6y7 蚤@(BXbM/袹p|&{Moݩ'F>>8O#{`n)آL6lmԕIfBBuEm-0l^K;CgHd\5uþd@s5ޓ,EKo3cHqjE O$8L?SEWfsbe%.IGre+TCk0L̖eqJ;⃌[ رaa5* yPo*t LZHz}5 :P)G>mbZNg2w~6j4ZmhvU? ÆOQo}OEWHW\ec䷻h5ożv}5ILf @VhF/n]ǰ!`2@E33AquoD= | [\& AL[b`kIH!1*8QM.X&, mTdT-J62vh7kDN.)!uA [ jX@}`{5 e\k+}Vw{tr4iXiM?E# \$"0ρX[GN|o ܻ;Gs?\șABQANKF;ehcYς3ߚpn!}.YKv,sKHO_x u#Jƀu )Pu~lxp,{,5 (b*7瀀! ;x=Q+Z\ "4O>)+Jp@_ƭ~ I~Udgؓȧ"BUu^3pϓUX_e,94| DٍʙJȓ`j+Ⱥu\6z??#t J(hNP_UQC?p0:w8R۵'ݷbD[Gn2x%r>7yēy42FԦg%o'Is^/)J m /̚?,}22'rS p?p$% yn!pI=W!% *ٽ >~^'QCX+0ŗiݗcf) YS p}>,n'Z*o'lń4A|A62I/6>EMj6eGj`mIV8{v oN4Kߨ||'L;H>-YJdTJR2_M3cPdZ;잯f@!{J3ahtw?&ƿxkqjߐ4QD\.>ĮDt~B&7V_~)^\ixN'1n_z2x:5,+bSӓ~_& m4P9B`zC-6`m)BhpnJYt8io*O̾%R@Z r*}~(M5OZ#t*Juxw_-U~RhTWO"xVoFĄx_i&Zx/&{WKÃJeWiD*F<eX5ɉu=Utln@Jӛ Bm4PN+oǽ*COc"yrQI.(>7@Pm,!0Ѭ2xT*cE >&d1<!Eؔk<6KDVr /xcpsQ[c'8g>@Ɇ(u34CSCG+q "T{|f-KUbU :Q0J0c" l=6W+K+hYӀ]Š|l L2d+yxu]2.T1S&ǀ C1AZN0^K^PZwK".CλZ_%[#_6EBiLR3& L4lSBtG@c>-uW183x=ط)7Jj߈o IU(ś/ y/A;lU/Y0c\ͳ8 qXLvA1tΚ;$ffYxMYg'ZS!L\URΑfF[_M,cP!-/\t͓ TU*\} mOh$ІOMJ鸔}S'VTEGd"VoV;^~Yr&+u &1qA(-N  DʄAc3W.n\2壤Kc`ǢK&(Wϣ>}NJ4 )bo+V`ɹg8͌X/Ј QtzKg[Dvt]bU' j|bC5edQ Qf#8\YI+ZDh_N j_y ~=MU׎u5^c4tFHxUn o(/}l"]/I) ddYBjItfn> dWG2*)"eC5~*~mNvcLGII6XhĆߕLaenF`[~yOi 1P#id{w̅>Ʃ'rB ?iV$l,fNc ܑ-ެUi#Z {$9~NIP\^Ǧ7uyRۘ*;dOfOtO*;sZ3wո]q-Y+9v5C9 ./\_lzl₟~WҙJ&d *-w!_yO鱖9Aj Gg߽GUr$ x瞧yAȇ&"%">!m?ː(ɪ.a_Yҏ/ܙVU}=!1eeVE7VK@izWў:jϩ_|9v,8B&tv<ĨmGYʤO\L},fud9rH .?$D\ gOdOG{!;73sXٞ38cAÙ֛¹{1 W[ĢYڃY^ƛΌ;#hND;4:W{33~q~&)GB |Zh( cLpv`dc c=˩ds!/TBHiJ'&_~1]6-5_`חn%/#~BLpW"鵵H{CM_G-X4ʋx$ -yӚ淗3 _mIiN{ u T]_ MSndKuj S"}^,G0hP¦C΄o% 5p}7ವqYGPIRb~tRՎKQ-h s=pkA> iؘyN=7`)l9@k7n ʳ0p27Fi6JS&/PҮ Ey&I:AJF VN_, (aS6"= Ami\"*< <>+DKŽZӧ tȍ#~d6dp֋1 cGY44KgHkdm u 7+ ;EњH4G$HaV 4AR3Pk҆9 hCȇ, FIENDB`pympress-1.7.1/pympress/share/pixmaps/pympress-64.png000066400000000000000000000110331415371354200226710ustar00rootroot00000000000000PNG  IHDR@@iqsBIT|d pHYsu85tEXtSoftwarewww.inkscape.org<IDATx͛yTՕǿW4,ͦ&ȘDAFqIѠ1Ç nq&&c\2&B%8F DYEn^޽'TwuUڠS{==缻D|ZB~)Ӭ g *Df|hY&c \f pD@4a#a$!տ /ZP0 @<|,ϋ!/5 5Ak!1DG[1{7BIʡar iך6eeExlR9X5[ 3Ɓu ӜT*EU gR0L*h.[>+,}룪_>m3G9,frOEq``h"s:QUE mvkP(1 0X6Tp j6G)7/Q ?agl]odHd0o*4,0C*SUw TjB3,I[[ ^`"2n q9 @l4ș-dG"rf(C˕RH1V*QDXZe` CvRPjJʚKUY~6LAai㯺[<2і2cu' I 3 (UaE`|#YU#D3?RpA7V?R1{&Xm 5;Ob{.'T=,RasgdP*``w` Q'j#|iE]=E |Y,{*а Q}h9Zqc*1Ɏa1VUAgnrTYʡR2H" R<`%8S{T]?װL4^^LE w׋+qY>.UMG,{V%sJ| @P{sBH>sCքԍcuC_{qf־uR ƥ(H-^z t\|]~|Ju-|(zuԾnZ 4[CJkR<[ ٵz3W')-Bñ \ǂ{rkfo8=^Y/X9f# mymǏݳ$k͓cm%%ɧN<EI$LD؀d 6(x>5t6y -/Snk2jh׏K|Y(2ʦ D R #o Mj%O3'%pi w)j\s唬{Q~ߗtb2sV[OiPy88@C4'- Jd!!g9'v^%|C]I*4I׺ףN ?zW 3HEd*wɐTG,62Bqt (}9p 2G<V>-cgTЪM146ӒK͔u, Wfec{(j~۴@Dwp<^f'Ȯܟ,b|G7VS hH1 7ѩ"_sw.FIEBGJ/UEv{.w[9E{&+Huc#DB%AJA}hq_rUJvѿB8tSщXstzNz~In#6l|@#nGs A#~kX7 ^;[0v>䁗ߟ: w.Mknj|{l0튏AAL$@(5v 3Q цEw\ ѧm+DqexRKo RC9xv?쯔/f.B_SqvoRJއqb9Ưz!#G73c pAh͏ˁV"94dy1&it8MS]hОM-':0~H3l"t]*8]+5K+| l*@89^aB[!Hķ[сJ.'^H׀BKZsf{ dLsPd텕b"ڤ,m i*ƇSg/Mr"aBv ] 3n\BqFoA}lӧe2LJ:# 0 _,AWdCk$Ū蠀lMx}g$3{oTvmu@m7BkP1o5L0kf$ -27̿6A{j:@k_KYVg X1{0ACpF@"*NSXbssQJyG.*V|Em}7ʈ'(x\b @C;rT):ZhK3NdY]͸PO` v徦v{?9 pS|QՒy Z,R.pʃRK@7lȩ=ϋbcOg ΰ~S!G6oq@OJj>#HOYY;{LÔ;l˺6)᣼xTy-zrצ-hMSfe Cv<ޅI&34',o ]up悋)۾)ՠ !LU乵)w%nG'a6U02ov@cNⅎ9e,@w xiQ+5`͸ *v.B Mpp~ 5|͚{eĂ=<ݱ:=4痣'{-à~A}we ٕfMk~7( (pHvξ'J "ُsx 2q?kr,rΑaM,id"A2(X[/JepyN<{3iV`P$dp .: 9hXx {_f ?O&;+9Y&ncWF$c,A k Ϙm K{`gqB~Lqn8eqOB4ln 1]~]W.Е5D3j|.ߦ':Ÿ0:t$۵psOY0R`ɹI}Y F|bXD7Z((},doq}|3j1-9: I/5/bp:]qˆu.Cn =,w:cn$( ڰCB<#Hq p1 5 Q3_+Uy^IwpR)Ԁ0T CEbF` MF1!cwhUy=x`Z'IENDB`pympress-1.7.1/pympress/share/pixmaps/pympress.ico000066400000000000000000002462761415371354200224530ustar00rootroot00000000000000 hV   F00 % (D(   t6w9p0Li&c]XTQmOR7LO ~Dw;r2l+g#b\W US%TPcjZRJ}Bw;r3m+h$b]YYZxmpJiaXQK}Ax9r3m,h$b`bwwjwBe7VɁoOx:s3m,i&h$%͋W8R5[Bk!%{?~Gx;s5q1Wʞ'B/kM֟sLSVNM~D{@pO>@285*l|`xHޛGRNgoϷɯªWrNavOn[=5ҶӒӿ͹ZߥE2ҔXAgumS{_l8OZJ꾾ʱĪ鋪ư69Jz>|A v9bq2m,j'f"c_\X U RQ^OPLNG|Axx<-굱.ixYxte??$rzQMHF/.%M rY(嬖iD;$vqa$ҐxVSO!E9̳ǮĩdBD.X9}u۝*hd]X \袊ھҾϹ˴ǯ^IΔhzљezs׻<Ҿϸn8®mQ&ޟڦ!|j-tyՅ ǽiU f W+\:4۰z,"L=ƪy"m#<:/ٴѹ̵Ȱū>"'ӿк̵ȮΑa#KO5E II+D‹ѻ7Ͷ}Q=3S:9 7G8b#dSYn 0~? a`C( @  `(j&i' h%9e a^\YV TRPeO"IKu6u7r3;o.k)h%f!c`^[YV SQPyOwI}C~D y>wv9s4p0n,k)i&f"ca^[YV SRQDPQPLMH~D{@y=v8s4q1n-k)i&f!ca^[YV TS`RSXYTOKH~D{Ay=v8s5q1n.k*i%f"da^\YW V [a W cg`~\WSPLH~E{@y=v9s5q1n.l)i&f"da^\ZY>YZnhkGgc_\WTPLI~D|@y=v9t6q2n-l*i&g"da^]]]vx rnkgd`[VRNJFD|Ay=w9t6q1n.l*i'g#da`eb]|~Czwsokffqk]]gvxYG~C|Ay=w:t6q2o.l+i'g"edeпzuzk[6"B>yɈqXE|@y=w9t6q2o/l*j'i%=i%zF,(Z6qG-N u2|ۄbE|Az>w:t6r3o.m-qo+!W I0m璅vF+kd*KE|Bz>w:t6r4o32稢`D#k} pe 22kPIF|Bz>x;w8Ŧɥ8견XB>-͓Qvrz0܂y RMJF}C|@ Τ3]HE5ާЭ;{zH- }fTRNKG1' i5*ةƨw@* ՅolCjYURG(ϴɲǭΦ⚕#(薈xtwU`]Zm]hXҽϺ̵ʱǮŪ{|[=}w4web9c ݝfҾϹ̵ʱ\BrZkikX ϩҽйŪť 4&觡\z_ڢwW^T@`˳践A2t᫗' Q$K5u0pYWDçN6. ޓV K^0'լ̲ȯū¨OQ(GD& ĤվкͶʳȰŬèS}bc kųE@Ҿлͷ˳ȯŪ;xe s*(yrkb[+=SҾϹn̵SdQ83*Ѳ̈Mӿ6?r =(s 1 /PWF, Wq w\K.>6g+ #c(0` $ i%f$f"!cSa_][YW V TSQPOONLl)l1l. k*:j'g$e!cb`^\ZYW U TRQPN5LOu6u7s44q1n-l*k(i%g#e!db`^][YW V TRQPO.INy<{@ xxx;v9t6r3q1o.m,l*j'h$f"dca`EbZ{~ zxvsqnligd`]^a~gzh|aWMHGE}C{Az>xx;v9t6s3q1o/m-l*j'h$g"f!)g"`!l~{xu}iO0  7ikz\JD}C|Az>x;v9u7s4q2o/n,l*j'i&^m&^'oA) )?%@(0a1kxME}C|Az>x269wG0[~x+ + KDh|wbN* C&UvQO =[IQ 'o/a _P ??|?(  U+[Y` ^1^L]h\[ZZYXX W V U V USTRvRZR>Q&LUM f a$d!bA`o`__^^]\[\ZYYX W V V U U USSRRQOOWQ,NPf` f&i''e"[d dcbaa`__^]]\[[ZYYX W W V U U TTRRRQPQOxQwy=yy=xy=xy=xy=xy=xy=xy=xy=xy=xy=xy=xy=yy=yy=yy=yy=yy=xy=xy=xy=xy=xy=xy=xy=xy=xy=xy=xy=xy=xy=y~}|h +)5p%8_KJIHGF~E~D}C|B|A{@{?z>y=yy=yy=yy=yy>y=xy=xy=xy=x*ڔ~}|{~VJpmmlkjihl)b2XONMLKJIHGFE~D}C}B|A{@{@z?y>y=xy=xz=x9$¦pSrQã yvutsrqponnmo 5cba`ϒ|]:SRQPONMLKJIHGFE~E~D}C|B|A{@z?z@,̦çen\|yxwvutsrqponn]8 edcbaf}0ҋsTSRQPONMLKJIHGGF~E~D}C|B|A{@4@èè§w /أ|{zyxwvutsrqpoktfedcbkUΌvUTSRQPONMLKJIIHGF~E~D}CzB2UƪƬīĩè"֝X ~}|{zyxwvutsrqنvhgfedcb3AgVUTSRQPONMLKJIIHGF~E|D)åƬ~ūūŗ ;,뮧ު+䥏~}|{zyxwvutt萀djihgfedf ucWVUTSRQPONMLKJIIHGD"̪ȰtǮե =.~}|{zyxwvvyUlkjihgfedZXWVUTSRQPONMLKJJIH ı ʲj嵹,!ﵫ&ܨ~}|{zyxwwXnmlkjihgfnGʎ|ZYXWVUTSRQPONMLKJMѹ ͳ[E7䮱§¦毜O2~}|{zyx^ponmlkjihggv \_[ZYXWVUTSRQPONMLIժG9֥Īĩ触ᱠ w~}|{z crqponmlkjihߕzs`\[ZYXWVUTSRQPONN $ ƛƭƬūĪĩ触~}똆itsrqponmlkjiy*3ǐ^]\[ZYXWVUTSRQQUȰȯǮƭƬūĪĩèç¦|ᮜƁrvutsrqponmlkj+ho_^]\[ZYXWVUTSRgN xmʳʲɱȰȯǮƭƬūŪĩèç¦]K簜iۋxwvutsrqponmlsv!a`_^]\[ZYXWVUSJI/=1ʷ̵˴ʳʲɱȰȯǮǭƬūŪĩèç¦︢V`|R|zyxwvutsrqponmۙMClba`_^]\[ZYXWW/ ޳θͷ̶̵˴ʳʲɱȰȯǮǭƬūŪĩèç¦丧:uV9/}|{zyxwvutsrqpoodcba`_^]\[ZYXd~лϺιθͷ̶̵˴ʳʲɱɰȯǮǭƬūŪĩèç¦)Y:~}|{zyxwvutsrqpoy6iedcba`_^]\[^L?ҾѽмлϺιθͷ̶̵˴˳ʲɱɰȯǮǭƬūŪĩéè§ָ+ÄY~}|{zyxwvutsrqϚ7gfedcba`_]\[f eҿҾѽмлϺιθͷͶ̵˴˳ʲɱɰȯǮǭƬŬūĪéè§溬 pMӍ~}|{zyxwvutsr:thgfedcbb``0UҿҾѽмлϺϹθͷͶ̵˴˳ʲɱɰȯǮǮƭŬūĪéè§̷!²'~}|{zyxwvut˜E oihgfedcc^f ҿҾѽѼлϺϹθͷͶ̵˴˳ʲɱɰȯǮǮƭŬūĪéôɌzS~}|{zyxwvx E|kjihgfeew`'"aǖӿҾѽѼлϺϹθͷͶ̵˴˳ʲɱɰȯǮǮƭŬū۽vS΍~}|{zyxw֟Hplkjigge:fg TӿҾѽѼлϺϹθͷͶ̵˴˳ʲɱɰȯǮǮƭ˺+ "~}|{zy~bznmlkjjhQ$ӿҾѽѼлϺϹθͷͶ̵˴˳ʲɱɰȯǮõ iuR~}|{z梆<{ponmljgj &˄ӿҾѽѼлϺϹθͷͶ̵˴˳ʲɱɰŴ.§Ȍ~}|vqpnoq-UxP<ӿҾѽѼлϺϹθͷͶ̵˴˳ʲ¼;è§j4%~}آLwsrqos͐ӿҾѽѼлϺϹθͷͶ̵̵ɷ> pîĪĩ触㤌[ڞutss<@ӿҾѽѼлϺϹθθͷǼ[UñƬūĪĩ触k4&ywutpEԔӿҾѽѼлϺϹθʽiCǮƭƬūĪĩ触ˤ5<}xvACӿҾѽѼлкϹùȰȯǮƭƬūĪĩ触bJ4&駎`'yuՆӿҾҽѼлʳɱȰȯǮƭƬūĪĩ<6K0ӿҾҽ̿~ʴʳʲɱȰȯǮƭƬ굲4'ϧ1@m&Z fӿKQȻ̵˴ʳʲɱȰȯǮP>P ! ؤm<ͷ̶̵˴ʳʲɱ}lS¦>ιθͷ̶̵˴౼ &èç¦ M T ;Ϻιθͷ.%ɜūŪĩèè§ի-svlHξмлϺȻ?3~cǮǭƬūūĪéè§ۮ< Q݆aҾѽνSD3(ƲɰȯǮǮƭŬūĪéè§_ H%ݟҿjW֪˳ʲɱɰȯǮǮƭŬūĪéè§W3ߵ65myͶ̵˴˳ʲɱɰȯǮǮƭŬūĪéè§=g Di"sxxaϹθͷͶ̵˴˳ʲɱɰȯǮǮƭŬūĪĩè§FO6vXXaPѼлϺϹθͷͶ̵˴˳ʲɱɰȯǮǮƭƬūĪĩè§%-TZ7>L?ҾѽѼлϺϹθͷͶ̵˴˳ʲɱɰȯȯǮƭƬūĪĩè§,aFm *:0ӿҾѽѼлϺϹθͷͶ̵˴˳ʲɱɱȰȯǮƭƬūĪĩè§~!(m  4!+$ӿҾѽѼлϺϹθͷͶ̵˴˴ʳʲɱȰȯǮƭƬūĪĩ触¦d PS%%ӿҾѽѼлϺϹθͷͷ̶̵˴ʳʲɱȰȯǮƭƬūĪĩè¦äLål"ڻӿҾѽѼлϺϹθθͷ̶̵˴ʳʲɱȰȯǮƭƬƫĪĨ9  T=7#,ӿҾѽѼлкϹθθͷ̶̵˴ʳʲɱȰȯǮǭƭǮ)ժS >y45ӿҾҽѼлкϹθθͷ̶̵˴ʳ˲ʲɰɮqij" T!kν0,JӿҾҽѼлкϹθθͷͶ̵˴ʳʱ>Ҵn5rj_9ӿҾҽѼлккθηθͷ\ѶժV6Mg` rjӿҾҾѼѻйϹuζ* 5JF""2+3$ҿҾӼoи+Ͽ2: P G%íf'̪aM.%oLGež~?ۿı  (vzT=& ?]{ o ͱʁL!ժ`X !BpF5ѭЇY.߿7ti0%Zt ڏنzcG-  bB?#= ~ =C   ;9qI&`U;T ]&&?5x+,D\[A(}H8-O }(B&B  &=<%y0biYn2U'.WhCxt:e.y)3b.uAl 7}h ????????????@8x???????pympress-1.7.1/pympress/share/pixmaps/pympress.png000066400000000000000000000301341415371354200224450ustar00rootroot00000000000000PNG  IHDR>asBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDATxy^E?U]zMgCHؑ%@A ,e`p`P7vp ?vHH, IBw:]s~ty}[Uo:uT]|$cћa1ma5U1IlVUz*%E=mKy?"@0!IǴH<1&Hu4UD`@(@ NOax Ϻ}0*?6 a(a8VQ>*ThF(`P„ Z/3:;o{U0o2y1À  jk[;; yzDPaTPUVY*MVTYhbfWQ2zx(d  .tU1aZM2Jj:u>;vdu>"@)(ζ_5=4CB0hj f ̳`-8$DKk[+UPrzm^1g` <@p8G1Rg=xiPcl *wY\TZ|y\Y"?T^n2-Y3%-"4bDavj#I&[ @BFbf(1QQ6˿7T`Hˍfh}5 e7 Ed~k0j=@ XN66LZ^fs #0N&N$@0]&E1*lQB* RǏKn0Ã*Uq1Lth˦1qpU{\Rn62&HkPJ1 I /BПbf7/[K jU2&0@Y3,+w*:γ&B]{faDYn9#[,qg9lE%䁱Q40N^ o13~h\h:.RU )~j\Ś(5b 7b3W6հ /›2"|%r/U븓4p@WBiD9|DhJo8upcP f v$u\L&¿;˷1&awr^1C@ `l6:/m ьɚz].=e!cWA*~WmWр bk]y  D < lVdBRfm&*41j"L$eCr{ uŠ,4k68q 6_d]9s·*)[G L5|V#.=U٨]zw{y6bfI({K24L54OҚJs@B]]J9XyBiOS9OIs1 D.jƉIQڥ\'q>hֈϣYא Dy,0^)ٖOc&1 SdKq<F4OCFL}(i:Z 36Oq~JmʏQ:OoAϯޞ-1*$Uj6%↔zɷ&ui,R[̹^DɮF* ?E.ҿ_IcnVF{K@I/8謪=`50ȶeS_l|utګ5XS$k?&ۧ]1A0("B` ()`VV|UѹMH>B*"%om).h=q|F/v "R{ŘK)׀*B>O53]Lv])aӣrfϿ;z]_e0N 5*T9?կ3k]ܵƦp)WqZfow_ Y0V]TI XLFx7XNӲ٣MQX+Y1db@QcEig̉Dq=v# wj1[*3Sx'hHX9٤`"Ķ.bKgfuI< U"@lZI"BZ QDX"']5;z!t˕L7|bTFl>Mi\]a3F @^ ֨{Q8Dz$N_!Lc YIݏ=HvvH**4cxEǽ'7lm˶H N1zF-FW @R|bDjQO55]nNF"7"@2^ljHW`۸o<_ZUIҪ8Wm|pWYkv >[LVU1Ey { 2w:۴l̈́ mKVoLs?KV`~Cw"~ug1^A4tFj1SnMyu3}"(Qe;F mX4jQxj5^~|Q`C=KA0`|u0U\:1ͮi&,}/x4O;W*~ ao.hinP-=Chp3GfgQĝS?3JcY'X::ވ!p?aZ,Y0@<P"Li_|iۀf'EdzK s`ڍ- Fj [ڊ0BؤAӈC@!=$fYEz"@N RL] ;eOQb\DR\k67#Xwyk,;~Ą{<{za1JD?ӇKL&$K94>b1Kc4-_aL12OAXǛԶ,4`lw¶'#X9{l:}%ԬYUHF 1ZPA=9H iEo J?F .6,\ݸq_ABKb & 6G@LC! /#O@${^E !ÔBo=0rgB=|9 LZscx*gT8Ff.-D4PkG[݁tħu7iﰼhx} 3yL qklĤmh02HC޿:v CU!Ey WE(ASQX{f9d쫉8$':u&gumy20tŋ v ݸ4LLvqdj"GU ss9r@K !7D)B?fW*m)ˬ5x fC7}M6/by;U*[HW{,`hեPk% Ʈc(_a/d Z\Es n#@àoΙtkܔBV] H5]Q{~6k@A J! ߑ#1MMhpU14Tz3(RyYlħkna% M; ^“bUԭjJN@/ "SL epdoXB&<*1(TA6n#5ltʩ׭U%y;+9na~~EE5  T]{sI v.} y_)ᩜ~D(_6Ō{гa*jLShd<jIn+~^PmQx&s'aL%j8Y|Ts<[ D( 9Ƀz-5+]9?7o? ^HPT FoiW}[1F N<7<jyc=g1Q/WrMsWcGT'I4>t ;"1.Y8K.ĈܻG^̄ _3ڮɎqV͍3ZGa`PO,q\I4 #RWS*ټDdGC2[#c23[ ')iIUճ6޿^>E_p(F|ՌIiC($PVy' crr{hܑy3qڻa"|Z3#5mԆpWNO0\DthF'OZ`Ljb{xmei4k)].n+Q3[ o:ly~½z,^L$3(_b DS: 1?I6KV~NYfި|?jɒMբ)ٞOCgL<,* +=ṵg~nH;O#ߗQBxiuGX ``}[m'OO2ϼ_ERnҌTt4(Nm=4ٴY}r6@ַfiIP/ tnJԆ>ÇS_c i۴;.ymVe^^ht' /"Gf>{|J%4S"ɓ@ggNр_5$#`ATX#'<@Y6`]9&ːѦY=/ToهAJr 10*lc<Xh63*%4F!pB &>&@<V;^@9/~h`m;I3a<넆 ͜a+"'b|IUT'4#ʕ:6gǼjE^{ʽk4 "V"lљ,"̦W=TRetlg78 Иa[hR$I{iY^wX6u*!z=߳1~ +f@շEnI+^g;_y6H |Z:"c2%cxHU-4nV T켇MyۺNȜ]5MnKR0Eaxm% $+Un)&=Z#jzhpMP%TV͢mLȈd薷˯W=b/؃ ̞G MɼN#;/F1}:µ?`Y?O刚CE3M&c¼p0*X cbm^]1Gdnah; ǰTN,Q&E})fUqsڜ3גf[b5LJFD u0Chf%ݼȢ(e= LPHqLN2%A}v'^TOXOpR/*$L,vkܲGE$@ eWٞ=15[s7%2h$hF%?2+gqډM>Pՙ{tȑvf۩aE_~٭( ~.*5 wO¤yzDp _WU1 P®{0Z1 6"VzEywslGP[ki&Nx[˝_!i 5~% 'P2ew!\<ѯ>'y$>xV;Ld4dgE`ϻe'ޑ| c{61L7Rn (H* _aGZ wcW$@4m5ލs{Q/p܅2U?՛6oH)%3*Ʈ.U{@,h>5o>qB쇌ٳ5ir**`/M>OﵾaD7Ae HaGnx >[mN<ƾ׊VϤ݌F1!gȺTg[S}J99'%ĖIw!s BTJ^Gi vO}?T:]{GuXzޏ >koˏc#UNy;2мq AI{-(Z7ϳHs#^ŰpgOfԚ0`oO^ngEÈ6_/{XyúCo:~MvPDCr|R> Ұ-`^:LW㧙9y}2*2n/_* VyܗunB9~"vY6"q9H/~YqJaűn]?`{f6+ͧqDg HP['73I^xmFu" lwg&O"wBIDATɆtg<`SLjS9^U!D*"~;޲>i_8}gh߱c_eĎL-Y_;oy F}&+̷#7ٹgRk]?=VUfHEHj6M:%W'\#W?_]_0XOz!]~8 Yzelg02u$ek@Ģ aSf%W݂i(6;|ڳ hI<~:L~=w[UG/'zkcL>`U|D}>+%yt>,@w;q!+1CM`PxL{p}( RDM gcxA/jS LWEP7Jgƀ6ww?iع#t4ᚘϝĎYJj|Ba*b+"ޝ5UNg;ИsI_Kja\NlR2JVS~p>{1һ*"&iA@-%ݪEx<!F& WrP_eʧXP{N]?E:Z8\X  B׸5ѮmYst׮qmaIj>7_ͤUa6qU pﰜ(#{+5dNX6jޚ}8ϡi4GNi4ZqxA|/02Xt}}wfVgyayЙ wH|c.c AE(=3;ګjmWv)=5u15rRi9an[h0[txJGx1]?DŽx`# .\mؑ3ttڠ{smG'f7ػ Dc0?y)8VoD;/%z7]bq5ȾiF6R=u]ӫT y&7MLVcEqUKQ۫ F1\I'wcDWs(u2{f6pأ2*g1}Mհ $k /~%™*c`j(v[3<Ũmkm-#QjQPH&`Ŕxc$uRyIre"51*sɸ!AΆ-1PkobdZ#UՒՒ֒V=gI0 A*??_{GO ȈH5ny V2Iͯf$-#V8kP7Y[#pIDhr,&fŤK=@>= 鈲F >Z:y, ꀣoTf{PP?6 IF Te5,|*g7dzBQ$+FO#|^R%U}%>E ] @TЄGVCh[(L"ka6J(Co̿A%/ HjPgTYJGZQe9{|%ӥ>N"&!WL*)+։jW $`E^N( ~}+ܯBd6z`]Q*}54b{@>~,S%UqkZ{[Ŝ}Xl=TPUkxS?_pi8?Z k%;|z_C |rVhqGQH*`t3#1|AQe4KgsWv*SXe˵B2hzOnfi^QIQ\="ktZWxa5@V,Lc0_afGz3WR=P2J;;[{-s{Bۈp ¡T>OcW>*BiQerW}4 > Sn 5Ue(~I֮2(,/ L rܽ}#(_*xEW"=ZL%ܟ'Tx5i˛zoGeKJ;>SS\0I533 U%PU:Hە! + WM){p*kE _sbҽ3JE١kޛ: E… 1WZt1=ŁVNS(Ų/$zde7'P&a9Jf2L!{h?BVEXH8x15ʂxo.c\JDR1Vx_H BRM-8J,4Y_G51FZ0`*.W0@ UհgDh>Y_KrX:1_ 2^BP  -jx] image/svg+xml pympress-1.7.1/pympress/share/xml/000077500000000000000000000000001415371354200171735ustar00rootroot00000000000000pympress-1.7.1/pympress/share/xml/autoplay.glade000066400000000000000000000206631415371354200220360ustar00rootroot00000000000000 99 99 1 10 0.10 100 3 1 10 1 100 100 1 10 False dialog False vertical 2 False end False False 0 True False Choose parameters for automatically playing slides True False 0 True False True False First slide right 1 True True 0 True True 3 3 autoplay_lower True True False 1 True False Last slide right 1 True True 2 True True 3 3 autoplay_upper True True False 3 True False 1 True False True False 5 5 Time per slide (s): True 1 True True 0 True True 5 5 autoplay_time 1 True True False 1 Loop True True False 20 0 True True True True 2 True False 3 pympress-1.7.1/pympress/share/xml/content.glade000066400000000000000000000054261415371354200216520ustar00rootroot00000000000000 True GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK | GDK_SCROLL_MASK Pympress Content content center True False True False True False 0 none 1.3300000429153442 False True False True True True False GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_STRUCTURE_MASK -1 pympress-1.7.1/pympress/share/xml/highlight.glade000066400000000000000000000551671415371354200221560ustar00rootroot00000000000000 2 90 8 1 1 False True False True False 0 none 1.3300000429153442 False True False GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK True False True True 0 True False 12 True False center center 5 5 vertical 5 60 60 True True False False app.highlight-use-pen '1' False True 0 60 60 True False False app.highlight-use-pen '2' False True 1 60 60 True False False app.highlight-use-pen '3' False True 2 60 60 True False False app.highlight-use-pen '4' False True 3 60 60 True False False app.highlight-use-pen '5' False True 4 60 60 True False False app.highlight-use-pen '6' False True 5 60 60 True False False app.highlight-use-pen '7' False True 6 60 60 True False False app.highlight-use-pen '8' False True 7 60 60 True False False app.highlight-use-pen '9' False True 8 60 60 True False False app.highlight-use-pen 'eraser' False True 9 False True 1 True False vertical gtk-close 80 80 True True True app.highlight True True False True 0 gtk-clear 80 80 True False True True app.highlight-clear True True False True 1 gtk-undo 80 80 True False True True app.highlight-undo True True False True 2 gtk-redo 80 80 True False True True center app.highlight-redo True True False True 3 True False center center vertical 80 80 True True True True True True False True 1 80 160 True True vertical scribble_width_adjust True 0 False bottom False True 2 True True 4 gtk-zoom-out 80 80 True False True True app.unzoom True True False True end 7 gtk-zoom-in 80 80 True True True app.zoom True True False True end 8 False True 2 pympress-1.7.1/pympress/share/xml/layout_dialog.glade000066400000000000000000000210601415371354200230240ustar00rootroot00000000000000 1 16 1 1 1 horizontal horizontal vertical vertical False Presentation timing breakdown 250 True normal north-east 400 False vertical 2 False True end False False 2 True False True False 1 True plain Notes Plain Note pages Highlighting False False True 0 True False True True True 1 False True 0 True True natural layout_treemodel True False vertical True True widget 6 resizeable 1 2 orientation True False orientations_model 0 3 4 next slides count True next_count_adjustment 5 5 True True 1 pympress-1.7.1/pympress/share/xml/media_overlay.glade000066400000000000000000000107241415371354200230150ustar00rootroot00000000000000 True False vertical True False True False True False media.play Play True gtk-media-play False True True False media.pause Pause True gtk-media-pause False True True False media.stop Stop True gtk-media-stop False True False True 0 True False right True True 5 1 False False end 0 True False GDK_BUTTON_PRESS_MASK | GDK_STRUCTURE_MASK True True end 1 pympress-1.7.1/pympress/share/xml/menu_bar.xml000066400000000000000000000232051415371354200215070ustar00rootroot00000000000000 _File
_Open app.pick-file Open _Recent app.list-recent-files _Close app.close-file _Save app.save-file _Save as app.save-file-as _Quit app.quit
_Presentation _Pause timer app.pause-timer _Reset timer app.reset-timer Set talk _Time app.edit-talk-time _Fullscreen app.content-fullscreen _Swap screens app.swap-screens _Notes mode app.notes-mode _Blank screen app.blank-screen Align _content app.align-content _Annotations app.annotations Big buttons app.big-buttons _Highlight app.highlight _Pointer
_Permanent app.pointer-mode continuous _Manual app.pointer-mode manual _Disabled app.pointer-mode disabled
_Red app.pointer-color red _Green app.pointer-color green _Blue app.pointer-color blue
Highlight mode
Clear on page change app.highlight-mode single-page Never clear (manually only) app.highlight-mode global Clear and restore per page number app.highlight-mode per-page Clear and restore per page label app.highlight-mode per-label
Tools next to slide app.highlight-tools-orientation vertical Tools below slide app.highlight-tools-orientation horizontal
_Zoom in app.zoom _Undo zoom app.unzoom Notes position _Right half of slide app.notes-pos right _Bottom half of slide app.notes-pos bottom _Left half of slide app.notes-pos left _Top half of slide app.notes-pos top _After slide pages app.notes-pos after Timing breakdown app.timing-report
_Navigation _Next app.next-page _Previous app.prev-page _First app.first-page _Last app.last-page _Go to... app.goto-page _Jump to label app.jumpto-label _Automatic navigation app.autoplay _Starting Configuration Content blanked app.start-blanked Content fullscreen app.start-content-fullscreen Presenter fullscreen app.start-presenter-fullscreen Portable installation app.portable-config Edit layout app.edit-layout _Help _About app.about _Shortcuts app.show-shortcuts
pympress-1.7.1/pympress/share/xml/presenter.glade000066400000000000000000002263211415371354200222060ustar00rootroot00000000000000 1 1 1 1 False GDK_POINTER_MOTION_MASK | GDK_KEY_PRESS_MASK | GDK_STRUCTURE_MASK | GDK_SCROLL_MASK Pympress Presenter presenter center True False vertical True False True True GDK_BUTTON_RELEASE_MASK 5 5 5 5 True True 600 True True False 5 0 in 1.3300000429153442 False True True False GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK True False Notes True True True True True vertical 11 True True False 5 9 vertical 200 False 0 in 0 1.3300000429153442 False True False True True True True False GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_STRUCTURE_MASK -1 True False Current slide True True True 0 200 True False True True True False True True 0 in 0 1.3300000429153442 False True True False True False Next slide True 0 0 True False True True 0 in 0 1.3300000429153442 False True True False True False 2nd next slide True 1 0 True False True True 0 in 0 1.3300000429153442 False True True False True False 3rd next slide True 2 0 True False True True 0 in 0 1.3300000429153442 False True True False True False 4th next slide True 3 0 True False True True 0 in 0 1.3300000429153442 False True True False True False 5th next slide True 0 1 True False True True 0 in 0 1.3300000429153442 False True True False True False 6th next slide True 1 1 True False True True 0 in 0 1.3300000429153442 False True True False True False 7th next slide True 2 1 True False True True 0 in 0 1.3300000429153442 False True True False True False 8th next slide True 3 1 True False True True 0 in 0 1.3300000429153442 False True True False True False 9th next slide True 0 2 True False True True 0 in 0 1.3300000429153442 False True True False True False 10th next slide True 1 2 True False True True 0 in 0 1.3300000429153442 False True True False True False 11th next slide True 2 2 True False True True 0 in 0 1.3300000429153442 False True True False True False 12th next slide True 3 2 True False True True 0 in 0 1.3300000429153442 False True True False True False 13th next slide True 0 3 True False True True 0 in 0 1.3300000429153442 False True True False True False 14th next slide True 1 3 True False True True 0 in 0 1.3300000429153442 False True True False True False 15th next slide True 2 3 True False True True 0 in 0 1.3300000429153442 False True True False True False 16th next slide True 3 3 True True 1 True True True False True True 0 in True False vertical True False end end start gtk-add True True True app.add-annotation True True True 0 gtk-remove True True True app.remove-annotation True True True 1 False False end 0 True True True True in True True True True immediate annotations_liststore False False True True 0 False True autosize annotations True True 0 False True end 1 True False Annotations True True True True True True True 0 True True 1 1 True False gtk-go-back True False True 10 app.prev-page True True True 2 0 True False 0 out True False True True False True True True 3 page label True True 0 True False end 0 right True False True True 2 True False end 0 right True False True True 2 True False start /0 False True True 4 True True 3 1 spin_adjust 1 True True if-valid True True 4 True False Slide number True True True 1 gtk-go-forward True False True 10 app.next-page True True True 2 2 Highlight True False True 10 app.highlight True True 2 3 Pointer True False True 10 app.pointer-mode 'toggle' True True 2 4 True False 1 0 in True False 00:00 15 True False Time elapsed True True True 5 True False 0 in True False True False 00:00 True False Time estimation True end True False False 6 True False 0 in True False 00:00 8 True False Clock True True False False 7 False True 2 pympress-1.7.1/pympress/share/xml/shortcuts.glade000066400000000000000000000276571415371354200222500ustar00rootroot00000000000000 1 1 navigating Presentation 16 1 Navigating 1 Right Down Page_Down space Next slide 1 Left Up Page_Up Previous slide 1 <shift>Right <shift>Down <shift>Page_Up Next slide with different label 1 <shift>Left <shift>Up <shift>Page_Up Previous slide with different label 1 Home First slide 1 End Last slide 1 BackSpace Go back in slide history 1 <shift>BackSpace Go forward in slide history 1 g Go to page number 1 j Jump to page label 1 Timers 1 p Pause Play/pause timer 1 r Reset timer 1 t Set estimated talk time 1 Additional features 1 a Toggle annotations 1 n Toggle notes mode 1 h Toggle highlighting 1 l Toggle laserpointer 1 z Zoom 1 u Unzoom 1 <ctrl>z Undo highlight stroke 1 Return Validate goto/jump destination 1 Escape Cancel goto/jump/highlighting/zooming 1 Manage files 1 o Open file 1 <ctrl>w Close file 1 <ctrl>s Save file 1 <ctrl><shift>s Save file as 1 q Quit 1 Windows 1 f F11 <ctrl>l Toggle fullscreen 1 s Swap windows 1 b Blank screen pympress-1.7.1/pympress/share/xml/time_report_dialog.glade000066400000000000000000000111441415371354200240420ustar00rootroot00000000000000 False Presentation timing breakdown 700 True normal north-east 700 False vertical 2 False True end False False 0 True True in True True natural False vertical True 500 name True 0 60 time 1 60 duration 2 60 slide 3 True True 1 pympress-1.7.1/pympress/surfacecache.py000066400000000000000000000272071415371354200202670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # surfacecache.py # # Copyright 2010 Thomas Jost # Copyright 2015 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.surfacecache` -- pages prerendering and caching -------------------------------------------------------------- This modules contains stuff needed for caching pages and prerendering them. This is done by the :class:`~pympress.surfacecache.SurfaceCache` class, using several `dict` of :class:`~cairo.ImageSurface` for storing rendered pages. The problem is, neither Gtk+ nor Poppler are particularly threadsafe. Hence the prerendering isn't really done in parallel in another thread, but scheduled on the main thread at idle times using GLib.idle_add(). """ import logging logger = logging.getLogger(__name__) import threading import functools import collections import gi import cairo gi.require_version('Gtk', '3.0') from gi.repository import GLib class SurfaceCache(object): """ Pages caching and prerendering made (almost) easy. Args: doc (:class:`~pympress.document.Document`): the current document max_pages (`int`): The maximum page number. """ #: The actual cache. The `dict`s keys are widget names and its values are #: :class:`~collections.OrderedDict`, whose keys are page numbers #: and values are instances of :class:`~cairo.ImageSurface`. #: In each :class:`~collections.OrderedDict` keys are ordered by #: Least Recently Used (get or set), when the size is beyond #: :attr:`max_pages`, pages are popped from the start of the cache. surface_cache = {} #: `dict` containing functions that return a :class:`~cairo.Surface` given a :class:`~cairo.Format`, #: width `int` and height `int`, see :meth:`~Gtk.Window.create_similar_image_surface` surface_factory = {} #: Size of the different managed widgets, as a `dict` of tuples surface_size = {} #: Type of document handled by each widget. It is a `dict`: its keys are #: widget names and its values are document types from ui. surface_type = {} #: Dictionary of :class:`~threading.Lock` used for managing conccurent #: accesses to :attr:`surface_cache` and :attr:`surface_size` locks = {} #: The current :class:`~pympress.document.Document`. doc = None #: :class:`~threading.Lock` used to manage conccurent accesses to :attr:`doc`. doc_lock = None #: Set of active widgets active_widgets = set() #: maximum number fo pages we keep in cache max_pages = 200 def __init__(self, doc, max_pages): self.max_pages = max_pages self.doc = doc self.doc_lock = threading.Lock() def add_widget(self, widget, wtype, prerender_enabled = True, zoomed = False): """ Add a widget to the list of widgets that have to be managed (for caching and prerendering). This creates new entries for ``widget_name`` in the needed internal data structures, and creates a new thread for prerendering pages for this widget. Args: widget (:class:`~Gtk.Widget`): The widget for which we need to cache wtype (`int`): type of document handled by the widget (see :attr:`surface_type`) prerender_enabled (`bool`): whether this widget is initially in the list of widgets to prerender zoomed (`bool`): whether we will cache a zoomed portion of the widget """ widget_name = widget.get_name().rstrip('0123456789') + ('_zoomed' if zoomed else '') with self.locks.setdefault(widget_name, threading.Lock()): self.surface_cache[widget_name] = collections.OrderedDict() self.surface_size[widget_name] = (-1, -1) self.surface_type[widget_name] = wtype self.surface_factory[widget_name] = functools.partial(self._create_surface, widget) if prerender_enabled and not zoomed: self.enable_prerender(widget_name) def swap_document(self, new_doc): """ Replaces the current document for which to cache slides with a new one. This function also clears the cached pages, since they now belong to an outdated document. Args: new_doc (:class:`~pympress.document.Document`): the new document """ with self.doc_lock: self.doc = new_doc for widget_name in self.locks: with self.locks[widget_name]: self.surface_cache[widget_name].clear() def disable_prerender(self, widget_name): """ Remove a widget from the ones to be prerendered. Args: widget_name (`str`): string used to identify a widget """ self.active_widgets.discard(widget_name) def enable_prerender(self, widget_name): """ Add a widget to the ones to be prerendered. Args: widget_name (`str`): string used to identify a widget """ self.active_widgets.add(widget_name) def set_widget_type(self, widget_name, wtype): """ Set the document type of a widget. Args: widget_name (`str`): string used to identify a widget wtype (`int`): type of document handled by the widget (see :attr:`surface_type`) """ with self.locks[widget_name]: if self.surface_type[widget_name] != wtype: self.surface_type[widget_name] = wtype self.surface_cache[widget_name].clear() def get_widget_type(self, widget_name): """ Get the document type of a widget. Args: widget_name (`str`): string used to identify a widget Returns: `int`: type of document handled by the widget (see :attr:`surface_type`) """ return self.surface_type[widget_name] def clear_cache(self, widget_name): """ Remove all cached values for a given widget. Useful for zoomed views. Args: widget_name (`str`): name of the widget that is resized """ with self.locks[widget_name]: self.surface_cache[widget_name].clear() def resize_widget(self, widget_name, width, height): """ Change the size of a registered widget, thus invalidating all the cached pages. Args: widget_name (`str`): name of the widget that is resized width (`int`): new width of the widget height (`int`): new height of the widget """ with self.locks[widget_name]: if (width, height) != self.surface_size[widget_name]: self.surface_cache[widget_name].clear() self.surface_size[widget_name] = (width, height) def get(self, widget_name, page_nb): """ Fetch a cached, prerendered page for the specified widget. Args: widget_name (`str`): name of the concerned widget page_nb (`int`): number of the page to fetch in the cache Returns: :class:`~cairo.ImageSurface`: the cached page if available, or `None` otherwise """ with self.locks[widget_name]: pc = self.surface_cache[widget_name] if page_nb in pc: pc.move_to_end(page_nb) return pc[page_nb] else: return None def set(self, widget_name, page_nb, val): """ Store a rendered page in the cache. Args: widget_name (`str`): name of the concerned widget page_nb (`int`): number of the page to store in the cache val (:class:`~cairo.ImageSurface`): content to store in the cache """ with self.locks[widget_name]: pc = self.surface_cache[widget_name] pc[page_nb] = val pc.move_to_end(page_nb) while len(pc) > self.max_pages: pc.popitem(False) def _create_surface(self, widget, fmt, width, height): """ Given a widget, create a cairo Image surface with appropriate size and scaling. Args: widget (:class:`~Gtk.Widget`): the widget for which we’re caching data. fmt (:class:`~cairo.Format`): the format for the new surface width (`int`): width of the new surface height (`int`): height of the new surface Returns: :class:`~cairo.ImageSurface`: a new image surface """ window = widget.get_window() scale = window.get_scale_factor() return window.create_similar_image_surface(fmt, width * scale, height * scale, scale) def prerender(self, page_nb): """ Queue a page for prerendering. The specified page will be prerendered for all the registered widgets. Args: page_nb (`int`): number of the page to be prerendered """ for name in self.active_widgets: GLib.idle_add(self.renderer, name, page_nb) def renderer(self, widget_name, page_nb): """ Rendering function. This function is meant to be scheduled on the GLib main loop. When run, it will go through the following steps: - check if the job's result is not already available in the cache - render it in a new :class:`~cairo.ImageSurface` if necessary - store it in the cache if it was not added there since the beginning of the process and the widget configuration is still valid Args: widget_name (`str`): name of the concerned widget page_nb (`int`): number of the page to store in the cache """ with self.locks[widget_name]: if page_nb in self.surface_cache[widget_name]: # Already in cache return GLib.SOURCE_REMOVE ww, wh = self.surface_size[widget_name] wtype = self.surface_type[widget_name] if ww < 0 or wh < 0: logger.warning('Widget {} with invalid size {}x{} when rendering'.format(widget_name, ww, wh)) return GLib.SOURCE_REMOVE with self.doc_lock: page = self.doc.page(page_nb) if page is None: return GLib.SOURCE_REMOVE # Render to a ImageSurface try: surface = self.surface_factory[widget_name](cairo.Format.RGB24, ww, wh) except AttributeError: logger.warning('Widget {} was not mapped when rendering'.format(widget_name), exc_info = True) return GLib.SOURCE_REMOVE context = cairo.Context(surface) page.render_cairo(context, ww, wh, wtype) del context # Save if possible and necessary with self.locks[widget_name]: pc = self.surface_cache[widget_name] if (ww, wh) == self.surface_size[widget_name] and page_nb not in pc: pc[page_nb] = surface pc.move_to_end(page_nb) while len(pc) > self.max_pages: pc.popitem(False) return GLib.SOURCE_REMOVE ## # Local Variables: # mode: python # indent-tabs-mode: nil # py-indent-offset: 4 # fill-column: 80 # end: pympress-1.7.1/pympress/talk_time.py000066400000000000000000000241441415371354200176210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # talk_time.py # # Copyright 2017 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.talk_time` -- Manages the clock of elapsed talk time ------------------------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import time import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GLib class TimeLabelColorer(object): """ Manage the colors of a label with a set of colors between which to fade, based on how much time remains. Times are given in seconds (<0 has run out of time). In between timestamps the color will interpolated linearly, outside of the intervals the closest color will be used. Args: label_time (:class:`Gtk.Label`): the label where the talk time is displayed """ #: The :class:`Gtk.Label` whose colors need updating label_time = None #: :class:`~Gdk.RGBA` The default color of the info labels label_color_default = None #: :class:`~Gtk.CssProvider` affecting the style context of the labels color_override = None #: `list` of tuples (`int`, :class:`~Gdk.RGBA`), which are the desired colors at the corresponding timestamps. #: Sorted on the timestamps. color_map = [] def __init__(self, label_time): self.label_time = label_time style_context = self.label_time.get_style_context() self.color_override = Gtk.CssProvider() style_context.add_provider(self.color_override, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 1) self.label_color_default = self.load_color_from_css(style_context) label_color_ett_reached = self.load_color_from_css(style_context, "ett-reached") label_color_ett_info = self.load_color_from_css(style_context, "ett-info") label_color_ett_warn = self.load_color_from_css(style_context, "ett-warn") self.color_map = [ ( 300, self.label_color_default), ( 0, label_color_ett_reached), (-150, label_color_ett_info), (-300, label_color_ett_warn) ] def load_color_from_css(self, style_context, class_name = None): """ Add class class_name to the time label and return its color. Args: label_time (:class:`Gtk.Label`): the label where the talk time is displayed style_context (:class:`~Gtk.StyleContext`): the CSS context managing the color of the label class_name (`str` or `None`): The name of the class, if any Returns: :class:`~Gdk.RGBA`: The color of the label with class "class_name" """ if class_name: style_context.add_class(class_name) self.label_time.show() color = style_context.get_color(Gtk.StateType.NORMAL) if class_name: style_context.remove_class(class_name) return color def default_color(self): """ Forces to reset the default colors on the label. """ self.color_override.load_from_data(''.encode('ascii')) def update_time_color(self, remaining): """ Update the color of the time label based on how much time is remaining. Args: remaining (`int`): Remaining time until estimated talk time is reached, in seconds. """ if (remaining <= 0 and remaining > -5) or (remaining <= -300 and remaining > -310): self.label_time.get_style_context().add_class("time-warn") else: self.label_time.get_style_context().remove_class("time-warn") prev_time, prev_color = None, None for timestamp, color in self.color_map: if remaining >= timestamp: break prev_time, prev_color = (timestamp, color) else: # if remaining < all timestamps, use only last color prev_color = None if prev_color: position = (remaining - prev_time) / (timestamp - prev_time) color_spec = '* {{color: mix({}, {}, {})}}'.format(prev_color.to_string(), color.to_string(), position) else: color_spec = '* {{color: {}}}'.format(color.to_string()) self.color_override.load_from_data(color_spec.encode('ascii')) class TimeCounter(object): """ A double counter, that displays the time elapsed in the talk and a clock. Args: builder (builder.Builder): The builder from which to load widgets. ett (`int`): the estimated time for the talk, in seconds. timing_tracker: (:class:`~pympress.extras.TimingReport`): to inform when the slides change autoplay: (:class:`~pympress.dialog.AutoPlay`): to adjust the timer display if we’re auto-playing/looping slides """ #: Elapsed time :class:`~Gtk.Label` label_time = None #: Clock :class:`~Gtk.Label` label_clock = None #: Time at which the counter was started, `int` in seconds as returned by :func:`~time.time()` restart_time = 0 #: Time elapsed since the beginning of the presentation, `int` in seconds elapsed_time = 0 #: Timer paused status, `bool` paused = True #: :class:`~TimeLabelColorer` that handles setting the colors of :attr:`label_time` label_colorer = None #: :class:`~pympress.editable_label.EstimatedTalkTime` that handles changing the ett ett = None #: The pause-timer :class:`~Gio.Action` pause_action = None #: The :class:`~pympress.extras.TimingReport`, needs to know when the slides change timing_tracker = None #: The :class:`~pympress.dialog.AutoPlay`, to adjust the timer display if we’re auto-playing/looping slides autoplay = None def __init__(self, builder, ett, timing_tracker, autoplay): super(TimeCounter, self).__init__() self.label_colorer = TimeLabelColorer(builder.get_object('label_time')) self.ett = ett self.timing_tracker = timing_tracker self.autoplay = autoplay builder.load_widgets(self) builder.setup_actions({ 'pause-timer': dict(activate=self.switch_pause, state=self.paused), 'reset-timer': dict(activate=self.reset_timer), }) self.pause_action = builder.get_application().lookup_action('pause-timer') # Setup timer for clocks GLib.timeout_add(250, self.update_time) def switch_pause(self, gaction, param=None): """ Switch the timer between paused mode and running (normal) mode. Returns: `bool`: whether the clock's pause was toggled. """ if self.paused: self.unpause() else: self.pause() return None def pause(self): """ Pause the timer if it is not paused, otherwise do nothing. Returns: `bool`: whether the clock's pause was toggled. """ if self.paused: return False self.paused = True self.pause_action.change_state(GLib.Variant.new_boolean(self.paused)) self.elapsed_time += time.time() - self.restart_time self.timing_tracker.end_time = self.elapsed_time if self.autoplay.is_looping(): self.autoplay.pause() self.update_time() return True def unpause(self): """ Unpause the timer if it is paused, otherwise do nothing. Returns: `bool`: whether the clock's pause was toggled. """ if not self.paused: return False self.restart_time = time.time() self.paused = False self.pause_action.change_state(GLib.Variant.new_boolean(self.paused)) if self.autoplay.is_looping(): self.autoplay.unpause() self.update_time() return True def reset_timer(self, *args): """ Reset the timer. """ self.timing_tracker.reset(self.current_time()) self.restart_time = time.time() self.elapsed_time = 0 if self.autoplay.is_looping(): self.autoplay.start_looping() self.update_time() def current_time(self): """ Returns the time elapsed in the presentation. Returns: `int`: the time since the presentation started in seconds. """ # Time elapsed since the beginning of the presentation if self.paused: return self.elapsed_time else: return self.elapsed_time + (time.time() - self.restart_time) def update_time(self): """ Update the timer and clock labels. Returns: `bool`: `True` (to prevent the timer from stopping) """ # Current time clock = time.strftime('%X') # '%H:%M:%S' elapsed = self.current_time() # Time elapsed since the beginning of the presentation if self.autoplay.is_looping(): first, stop, loop, delay = self.autoplay.get_page_range() display_time = '{} {}-{} / {:.1f}s'.format(_('Loop') if loop else _('Auto'), first + 1, stop, delay / 1000) else: display_time = '{:02}:{:02}'.format(*divmod(int(elapsed), 60)) if self.paused: display_time += ' ' + _('(paused)') self.label_time.set_text(display_time) self.label_clock.set_text(clock) if not self.paused: self.timing_tracker.end_time = elapsed if self.ett.est_time: self.label_colorer.update_time_color(self.ett.est_time - elapsed) else: self.label_colorer.default_color() return True pympress-1.7.1/pympress/ui.py000066400000000000000000002120171415371354200162630ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # ui.py # # Copyright 2010 Thomas Jost # Copyright 2015 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.ui` -- GUI management ------------------------------------ This module contains the whole graphical user interface of pympress, which is made of two separate windows: the Content window, which displays only the current page in full size, and the Presenter window, which displays both the current and the next page, as well as a time counter and a clock. Both windows are managed by the :class:`~pympress.ui.UI` class. """ import logging logger = logging.getLogger(__name__) import pathlib import math import sys import gc from urllib.request import url2pathname from urllib.parse import urlsplit import gi import cairo gi.require_version('Gtk', '3.0') from gi.repository import GObject, Gtk, Gdk, GLib, GdkPixbuf, Gio from pympress import document, surfacecache, util, pointer, scribble, builder, talk_time, dialog, extras, editable_label class UI(builder.Builder): """ Pympress GUI management. """ #: The :class:`~pympress.app.Pympress` instance app = None #: Content window, as a :class:`~Gtk.Window` instance. c_win = None #: :class:`~Gtk.AspectFrame` for the Content window. c_frame = None #: :class:`~Gtk.DrawingArea` for the Content window. c_da = None #: Presenter window, as a :class:`~Gtk.Window` instance. p_win = None #: :class:`~Gtk.Box` for the Presenter window. p_central = None #: :class:`~Gtk.AspectFrame` for the current slide in the Presenter window. p_frame_notes = None #: :class:`~Gtk.DrawingArea` for the current slide in the Presenter window. p_da_notes = None #: :class:`~Gtk.Grid` for the next slide(s) in the Presenter window. grid_next = None #: `list` of :class:`~Gtk.AspectFrame` for the next slide in the Presenter window. p_frames_next = None #: :class:`~Gtk.DrawingArea` for the next slide in the Presenter window. p_das_next = None #: `int` the number of next slides currently on display in the “Next slides” pane, initialized to the maximal number next_frames_count = 16 #: `list` of :class:`~Gtk.AspectFrame` for the current slide copy in the Presenter window. p_frame_cur = None #: :class:`~Gtk.DrawingArea` for the current slide copy in the Presenter window. p_da_cur = None #: :class:`~Gtk.Frame` for the annotations in the Presenter window. p_frame_annot = None #: Indicates whether we should delay redraws on some drawing areas to fluidify resizing gtk.paned resize_panes = False #: Tracks return values of GLib.timeout_add to cancel gtk.paned's redraw callbacks redraw_timeout = 0 #: Whether to use notes mode or not notes_mode = document.PdfPage.NONE #: Current choice of mode to toggle notes chosen_notes_mode = document.PdfPage.RIGHT #: Whether to display annotations or not show_annotations = True #: Whether to display big buttons or not show_bigbuttons = True #: :class:`~Gtk.ToolButton` big button for touch screens, go to previous slide prev_button = None #: :class:`~Gtk.ToolButton` big button for touch screens, go to next slide next_button = None #: :class:`~Gtk.ToolButton` big button for touch screens, go toggle the pointer laser_button = None #: :class:`~Gtk.ToolButton` big button for touch screens, go to scribble on screen highlight_button = None #: number of page currently displayed in Content window's miniatures current_page = -1 #: number of page currently displayed in Presenter window's miniatures preview_page = -1 #: track whether we blank the screen blanked = False #: Dictionary of :class:`~Gtk.Widget` from the presenter window that can be dynamically rearranged placeable_widgets = {} #: Map of :class:`~Gtk.Paned` to the relative position (`float` between 0 and 1) of its handle pane_handle_pos = {} #: :class:`~pympress.config.Config` to remember preferences config = None #: :class:`~pympress.surfacecache.SurfaceCache` instance. cache = None #: Current :class:`~pympress.document.Document` instance. doc = document.EmptyDocument() #: Class :class:`~pympress.scribble.Scribble` managing drawing by the user on top of the current slide. scribbler = None #: Class :class:`~pympress.extras.Annotations` managing the display of annotations annotations = None #: Class :class:`~pympress.extras.Media` managing keeping track of and callbacks on media overlays medias = None #: Class :class:`~pympress.extras.Zoom` managing the zoom level of the current slide. zoom = None #: Software-implemented laser pointer, :class:`~pympress.pointer.Pointer` laser = None #: :class:`~pympress.editable_label.PageNumber` displaying and setting current page numbers page_number = None #: :class:`~pympress.editable_label.EstimatedTalkTime` to set estimated/remaining talk time est_time = None #: :class:`~pympress.dialog.TimingReport` popup to show how much time was spent on which part timing = None #: :class:`~pympress.talk_time.TimeCounter` clock tracking talk time (elapsed, and remaining) talk_time = None #: A :class:`~Gtk.ShortcutsWindow` to show the shortcuts shortcuts_window = None #: :class:`~pympress.dialog.LayoutEditor` popup to configure the layouts of the presenter window layout_editor = None #: :class:`~pympress.dialog.AutoPlay` popup to configure automatic playing autoplay = None #: A :class:`~Gtk.AccelGroup` to store the shortcuts accel_group = None #: A :class:`~Gio.Menu` to display the recent files to open recent_menu = None #: A :class:`~pympress.extras.FileWatcher` object to reload modified files file_watcher = None #: `int` or `None`, may keep track of the Gtk.Application inhibit request inhibit_cookie = None ############################################################################## ############################# UI setup ############################# ############################################################################## def __init__(self, app, config): super(UI, self).__init__() self.app = app self.config = config self.blanked = self.config.getboolean('content', 'start_blanked') Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), util.load_style_provider(Gtk.CssProvider()), Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) # We may want some additional CSS changes self.css_provider = Gtk.CssProvider() Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) self.show_annotations = self.config.getboolean('presenter', 'show_annotations') self.chosen_notes_mode = document.PdfPage[self.config.get('notes position', 'horizontal').upper()] self.show_bigbuttons = self.config.getboolean('presenter', 'show_bigbuttons') # Surface cache self.cache = surfacecache.SurfaceCache(self.doc, self.config.getint('cache', 'maxpages')) # Make and populate windows self.load_ui('presenter') self.load_ui('content') self.app.add_window(self.p_win) self.app.add_window(self.c_win) self.load_ui('menu_bar', ext='.xml') self.app.set_menubar(self.get_object('menu_bar')) self.recent_menu = self.get_object('recent_menu') c_full = self.config.getboolean('content', 'start_fullscreen') p_full = self.config.getboolean('presenter', 'start_fullscreen') self.setup_actions({ 'quit': dict(activate=self.app.quit), 'about': dict(activate=self.menu_about), 'big-buttons': dict(activate=self.switch_bigbuttons, state=self.show_bigbuttons), 'show-shortcuts': dict(activate=self.show_shortcuts), 'content-fullscreen': dict(activate=self.switch_fullscreen, state=c_full), 'presenter-fullscreen': dict(activate=self.switch_fullscreen, state=p_full), 'swap-screens': dict(activate=self.swap_screens), 'blank-screen': dict(activate=self.switch_blanked, state=self.blanked), 'notes-mode': dict(activate=self.switch_mode, state=False), 'notes-pos': dict(activate=self.change_notes_pos, parameter_type=str, state=self.chosen_notes_mode.name.lower()), 'annotations': dict(activate=self.switch_annotations, state=self.show_annotations), 'validate-input': dict(activate=self.validate_current_input), 'cancel-input': dict(activate=self.cancel_current_input), 'align-content': dict(activate=self.adjust_frame_position), 'next-frames': dict(activate=self.reconfigure_next_frames, parameter_type=int, state=self.next_frames_count), }) self.setup_actions({ 'open-file': dict(activate=self.open_file, parameter_type=str), 'close-file': dict(activate=self.close_file), 'save-file': dict(activate=self.save_file), 'save-file-as': dict(activate=self.save_file_as), 'pick-file': dict(activate=self.pick_file), 'list-recent-files': dict(change_state=self.populate_recent_menu, state=False), 'page': dict(activate=lambda gaction, param: self.goto_page(param.get_int64()), parameter_type=int, state=self.current_page), 'next-page': dict(activate=self.doc_goto_next), 'next-label': dict(activate=self.doc_label_next), 'prev-page': dict(activate=self.doc_goto_prev), 'prev-label': dict(activate=self.doc_label_prev), 'hist-back': dict(activate=self.doc_hist_prev), 'hist-forward': dict(activate=self.doc_hist_next), 'first-page': dict(activate=self.doc_goto_home), 'last-page': dict(activate=self.doc_goto_end), }) self.zoom = extras.Zoom(self) self.scribbler = scribble.Scribbler(self.config, self, self.notes_mode) self.annotations = extras.Annotations(self) self.medias = extras.Media(self, self.config) self.laser = pointer.Pointer(self.config, self) self.est_time = editable_label.EstimatedTalkTime(self) self.page_number = editable_label.PageNumber(self, self.config.getboolean('presenter', 'scroll_number')) self.timing = dialog.TimingReport(self) self.autoplay = dialog.AutoPlay(self) self.talk_time = talk_time.TimeCounter(self, self.est_time, self.timing, self.autoplay) self.layout_editor = dialog.LayoutEditor(self, self.config) self.file_watcher = extras.FileWatcher() self.config.register_actions(self) # Get placeable widgets. NB, get the highlight one manually from the scribbler class self.placeable_widgets = { name: self.get_object(widget_name) for name, widget_name in self.config.placeable_widgets.items() } self.placeable_widgets['highlight'] = self.scribbler.scribble_overlay # Initialize windows self.make_cwin() self.make_pwin() self.connect_signals(self) for action, shortcut_list in self.config.shortcuts.items(): self.app.set_accels_for_action('app.' + action, shortcut_list) # Common to both windows self.load_icons() # Adjust default visibility of items self.prev_button.set_no_show_all(True) self.next_button.set_no_show_all(True) self.laser_button.set_no_show_all(True) self.highlight_button.set_no_show_all(True) self.p_frame_annot.set_no_show_all(True) self.prev_button.set_visible(self.show_bigbuttons) self.next_button.set_visible(self.show_bigbuttons) self.laser_button.set_visible(self.show_bigbuttons) self.highlight_button.set_visible(self.show_bigbuttons) self.p_frame_annot.set_visible(self.show_annotations) self.laser.activate_pointermode() # Setup screens and show all windows self.setup_screens() self.c_win.show_all() self.p_win.show_all() # Queue some redraws self.c_da.queue_draw() self.redraw_panes() self.do_page_change(unpause=False) def load_icons(self): """ Set the icon list for both windows. """ try: icon_list = [GdkPixbuf.Pixbuf.new_from_file(i) for i in util.list_icons()] except Exception: logger.exception('Error loading icons') return self.c_win.set_icon_list(icon_list) self.p_win.set_icon_list(icon_list) def make_cwin(self): """ Initializes the content window. """ self.c_frame.set_property('yalign', self.config.getfloat('content', 'yalign')) self.c_frame.set_property('xalign', self.config.getfloat('content', 'xalign')) page_type = self.notes_mode.complement() self.cache.add_widget(self.c_da, page_type) self.cache.add_widget(self.c_da, page_type, zoomed = True) self.c_frame.set_property("ratio", self.doc.page(self.current_page).get_aspect_ratio(page_type)) colourclass = 'white' if self.config.getboolean('content', 'white_blanking') else 'black' self.c_da.get_style_context().add_class(colourclass) self.c_win.get_style_context().add_class(colourclass) self.c_win.insert_action_group('content', self.c_win.get_action_group('win')) self.c_win.insert_action_group('win', None) def make_pwin(self): """ Initializes the presenter window. """ layout = self.config.get_layout('notes' if self.notes_mode else 'plain') pane_handles = self.replace_layout(layout, self.p_central, self.placeable_widgets, self.on_pane_event) self.pane_handle_pos.update(pane_handles) self.p_frames_next = [self.get_object('p_frame_next{}'.format(n)) for n in range(self.next_frames_count)] self.p_das_next = [self.get_object('p_da_next{}'.format(n)) for n in range(self.next_frames_count)] self.reconfigure_next_frames(None, GLib.Variant.new_int64(self.config.getint('presenter', 'next_slide_count'))) slide_type = self.notes_mode.complement() self.cache.add_widget(self.p_da_cur, slide_type) self.cache.add_widget(self.p_da_cur, slide_type, zoomed = True) # A single cache for all next slides self.cache.add_widget(self.p_das_next[0], slide_type) self.cache.add_widget(self.p_da_notes, self.notes_mode, prerender_enabled = bool(self.notes_mode)) self.cache.add_widget(self.scribbler.scribble_p_da, slide_type, prerender_enabled = False) self.cache.add_widget(self.scribbler.scribble_p_da, slide_type, zoomed = True) # set default value self.page_number.set_last(self.doc.pages_number()) # Enable dropping files onto the window self.p_win.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) self.p_win.drag_dest_add_text_targets() self.p_win.insert_action_group('presenter', self.c_win.get_action_group('win')) self.p_win.insert_action_group('win', None) def reconfigure_next_frames(self, gaction, param): """ Set the number of next frames to preview the the “next slides” panel Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the number of slides as a GVariant """ n_frames = param.get_int64() if n_frames < 1: n_frames = 1 if n_frames > 16: n_frames = 16 for n in range(self.next_frames_count): self.grid_next.remove(self.p_frames_next[n]) for n in range(4): self.grid_next.remove_row(n) self.grid_next.remove_column(n) rows = int(round(math.sqrt(n_frames))) cols = (n_frames + rows - 1) // rows for n in range(n_frames): self.grid_next.attach(self.p_frames_next[n], n % cols, n // cols, 1, 1) self.next_frames_count = n_frames self.app.set_action_state('next-frames', n_frames) self.config.set('presenter', 'next_slide_count', str(n_frames)) def setup_screens(self): """ Sets up the position of the windows. """ self.p_win.parse_geometry(self.config.get('presenter', 'geometry')) self.c_win.parse_geometry(self.config.get('content', 'geometry')) c_full = self.app.get_action_state('content-fullscreen') p_full = self.app.get_action_state('presenter-fullscreen') if not c_full and not p_full: # Just restored window sizes, that’s enough return # If multiple monitors, apply windows to monitors according to config screen = self.p_win.get_screen() if screen.get_n_monitors() <= 1: logger.warning(_('Not starting content or presenter window full screen ' + 'because there is only one monitor')) c_full, p_full = False, False else: # To start fullscreen, we need to ensure windows are on individual monitors c_monitor = screen.get_monitor_at_point(*self.c_win.get_position()) p_monitor = screen.get_monitor_at_point(*self.p_win.get_position()) primary = screen.get_primary_monitor() if c_monitor == p_monitor: warning = _('Content and presenter window must not be on the same monitor if you start full screen!') logger.warning(warning) if p_monitor == primary: # move content somewhere else self.move_window(screen, self.c_win, c_monitor, (primary + 1) % screen.get_n_monitors()) else: # move presenter to primary self.move_window(screen, self.p_win, p_monitor, primary) if p_full: self.p_win.fullscreen() if c_full: self.c_win.fullscreen() self.set_screensaver(True) self.app.set_action_state('content-fullscreen', c_full) self.app.set_action_state('presenter-fullscreen', p_full) def move_window(self, screen, win, from_monitor, to_monitor): """ Move window from monitor number from_monitor to monitor to_monitor. """ x, y, w, h = win.get_position() + win.get_size() win_state = win.get_window().get_state() if win.get_window() is not None else 0 if (win_state & Gdk.WindowState.FULLSCREEN) != 0: win.unfullscreen() if (win_state & Gdk.WindowState.MAXIMIZED) != 0: win.unmaximize() to_bounds = screen.get_monitor_geometry(to_monitor) to_w = min(w, to_bounds.width) to_h = min(h, to_bounds.height) from_bounds = screen.get_monitor_geometry(from_monitor) # Get fraction of free space that is left or top of window x = (max(0, x - from_bounds.x) / (from_bounds.width - w)) if w < from_bounds.width else 0 y = (max(0, y - from_bounds.y) / (from_bounds.height - h)) if h < from_bounds.height else 0 win.resize(to_w, to_h) win.move(to_bounds.x + x * (to_bounds.width - to_w), to_bounds.y + y * (to_bounds.height - to_h)) if (win_state & Gdk.WindowState.MAXIMIZED) != 0: win.maximize() if (win_state & Gdk.WindowState.FULLSCREEN) != 0: win.fullscreen() def show_shortcuts(self, *args): """ Display the shortcuts window. """ # Use a different builder to load and be able to release the shortcuts window shortcuts_builder = builder.Builder() shortcuts_builder.load_ui('shortcuts') self.shortcuts_window = shortcuts_builder.get_object('shortcuts_window') for command, shortcut_list in self.config.items('shortcuts'): display_shortcut = shortcuts_builder.get_object('shortcut_' + command) if display_shortcut is not None: display_shortcut.props.accelerator = shortcut_list self.shortcuts_window.set_transient_for(self.p_win) self.shortcuts_window.show_all() self.shortcuts_window.present() def close_shortcuts(self, *args): """ Destroy the shortcuts window once it is hidden. """ self.shortcuts_window.destroy() self.shortcuts_window = None ############################################################################## ############################ Dynamic resize ############################## ############################################################################## def on_configure_da(self, widget, event): """ Manage "configure" events for all drawing areas, e.g. resizes. We tell the local :class:`~pympress.surfacecache.SurfaceCache` cache about it, so that it can invalidate its internal cache for the specified widget and pre-render next pages at a correct size. Warning: Some not-explicitly sent signals contain wrong values! Just don't resize in that case, since these always seem to happen after a correct signal that was sent explicitly. Args: widget (:class:`~Gtk.Widget`): the widget which has been resized event (:class:`~Gdk.Event`): the GTK event, which contains the new dimensions of the widget """ # Don't trust those if not event.send_event: return self.cache.resize_widget(widget.get_name().rstrip('0123456789'), event.width, event.height) if widget is self.c_da: self.medias.resize('content') self.scribbler.reset_scribble_cache() self.scribbler.prerender() elif widget is self.p_da_cur: self.medias.resize('presenter') def on_configure_win(self, widget, event): """ Manage "configure" events for both window widgets. Args: widget (:class:`~Gtk.Widget`): the window which has been moved or resized event (:class:`~Gdk.Event`): the GTK event, which contains the new dimensions of the widget """ geom = '{}x{}{:+}{:+}'.format(*widget.get_size(), *widget.get_position()) if widget is self.p_win: self.config.set('presenter', 'geometry', geom) cw = self.p_central.get_allocated_width() ch = self.p_central.get_allocated_height() self.scribbler.off_render.set_size_request(cw, ch) self.adjust_bottom_bar_font() elif widget is self.c_win: self.config.set('content', 'geometry', geom) def adjust_bottom_bar_font(self): """ Scale baseline font size of bottom bar, clipped to 6px..13px. Fonts are then scaled by CSS em indications. """ ww, wh = self.p_win.get_size() font_size = max(6, min(13, ww / 120 if self.show_bigbuttons else ww / 75)) self.css_provider.load_from_data('#bottom {{ font-size: {:.1f}px; }}'.format(font_size).encode()) def redraw_panes(self): """ Handler for :class:`~Gtk.Paned`'s resizing signal. Used for delayed drawing events of drawing areas inside the panes. This is very useful on windows where resizing gets sluggish if we try to redraw while resizing. """ self.resize_panes = False self.p_da_cur.queue_draw() for da in self.p_das_next: da.queue_draw() if self.notes_mode: self.p_da_notes.queue_draw() if self.redraw_timeout: self.redraw_timeout = 0 self.config.update_layout_from_widgets(self.layout_name(self.notes_mode), self.p_central.get_children()[0], self.pane_handle_pos) def on_pane_event(self, widget, evt): """ Signal handler for gtk.paned events. This function allows one to delay drawing events when resizing, and to speed up redrawing when moving the middle pane is done (which happens at the end of a mouse resize) Args: widget (:class:`~Gtk.Widget`): the widget in which the event occurred (ignored) evt (:class:`~Gdk.Event`): the event that occurred """ if type(evt) == Gdk.EventButton and evt.type == Gdk.EventType.BUTTON_RELEASE: self.redraw_panes() elif type(evt) == GObject.GParamSpec and evt.name == "position": self.resize_panes = True if self.redraw_timeout: GLib.Source.remove(self.redraw_timeout) self.redraw_timeout = GLib.timeout_add(200, self.redraw_panes) ############################################################################ ############################ Program lifetime ############################ ############################################################################ def cleanup(self, *args): """ Save configuration and exit the main loop. """ self.scribbler.disable_scribbling() self.medias.hide_all() self.doc.cleanup_media_files() if self.app.get_action_state('content-fullscreen'): # In case we used hard-disabling self.set_screensaver(disabled=False) def menu_about(self, *args): """ Display the "About pympress" dialog. Handles clicks on the "about" menu. """ about = Gtk.AboutDialog(transient_for = self.p_win) pympress = util.get_pympress_meta() about.set_program_name('pympress') about.set_version(pympress['version']) about.set_copyright(_('Contributors:') + '\n' + pympress['contributors']) about.set_comments(_('pympress is a little PDF reader written in Python ' + 'using Poppler for PDF rendering and GTK for the GUI.\n') + _('Some preferences are saved in ') + str(self.config.path_to_config()) + '\n' + _('Resources are loaded from ') + str(util.get_locale_dir().parent) + '\n' + _('The log is written to ') + str(util.get_log_path()) + '\n\n' + _('Media support uses {}.').format(self.medias.backend_version) + '\n' + _('Python version {}').format(sys.version)) about.set_website('https://github.com/Cimbali/pympress') try: about.set_logo(GdkPixbuf.Pixbuf.new_from_file(util.get_icon_path('pympress.png'))) except Exception: logger.exception(_('Error loading icon for about window')) about.run() about.destroy() ############################################################################## ############################ Document manangement ############################ ############################################################################## def swap_document(self, doc_uri, page=0, reloading=False): """ Replace the currently open document with a new one. The new document is possibly and EmptyDocument if doc_uri is None. The state of the ui and cache are updated accordingly. Args: doc_uri (`str`): the URI to the new document page (`int`): the page at which to start the presentation reloading (`bool`): whether we are reloading or detecting stuff from the document """ if self.unsaved_changes(reloading): # If we keep the unsaved changes and were prompted by automatic file change, # stop watching for file changes in the future. if reloading: self.file_watcher.stop_watching() return run_gc = self.doc.doc is not None try: self.doc = document.Document.create(self, doc_uri) if not reloading and doc_uri: Gtk.RecentManager.get_default().add_item(doc_uri) self.file_watcher.watch_file(doc_uri, self.reload_document) elif not reloading: self.file_watcher.stop_watching() except GLib.Error: if reloading: return self.doc = document.Document.create(self, None) self.error_opening_file(doc_uri) self.file_watcher.stop_watching() self.current_page = self.preview_page = self.doc.goto(page) self.doc.goto(self.current_page) # Guess notes mode by default if the document has notes if not reloading: hpref = self.config.get('notes position', 'horizontal') vpref = self.config.get('notes position', 'vertical') target_mode = self.doc.guess_notes(hpref, vpref, self.current_page) if self.notes_mode != target_mode: self.switch_mode('notes-mode', target_mode=target_mode) # don't toggle from NONE to NONE if target_mode: self.app.activate_action('notes-pos', target_mode.name.lower()) else: self.doc.set_notes_after(self.notes_mode.direction() == 'page number') # Some things that need updating self.cache.swap_document(self.doc) self.page_number.set_last(self.doc.pages_number()) self.page_number.enable_labels(self.doc.has_labels()) self.autoplay.set_doc_pages(self.doc.pages_number()) self.medias.purge_media_overlays() self.timing.set_document_metadata(self.doc.get_structure().copy(), self.doc.page_labels[:]) # A new document, restart at time 0, paused if not reloading: self.talk_time.pause() self.talk_time.reset_timer() self.page_number.setup_doc_callbacks(self.doc) self.do_page_change(unpause=False) # Now that all references to the old document have been replaced or removed, manually # collect garbage to delete objects and release file handles / close file descriptors if run_gc: gc.collect(1) def reload_document(self): """ Reload the current document. """ self.swap_document(self.doc.get_uri(), page=self.current_page, reloading=True) def populate_recent_menu(self, gaction, is_opening=None): """ Callback for the recent document menu. Gets the URI and requests the document swap. Args: gaction (:class:`~Gio.Action`): the action triggering the call is_opening (:class:`~GLib.Variant`): a wrapped boolean indicating whether the menu is opening or closing. """ if not is_opening.get_boolean(): self.recent_menu.remove_all() return for file in Gtk.RecentManager.get_default().get_items(): if not file.exists() or not file.get_mime_type() == 'application/pdf': continue item = Gio.MenuItem.new(file.get_display_name(), 'app.open-file') item.set_action_and_target_value('app.open-file', GLib.Variant.new_string(file.get_uri())) item.set_icon(file.get_gicon()) self.recent_menu.append_item(item) if self.recent_menu.get_n_items() >= 10: break def on_drag_drop(self, widget, drag_context, x, y, data, info, time): """ Receive the drag-drops (as text only). If a file is dropped, open it. Args: widget (:class:`~Gtk.Widget`): The widget on which the dragged item was dropped drag_context (:class:`~Gdk.DragContext`): Context object of the dragging x (`float`): position of the drop y (`float`): position of the drop data (:class:`~Gtk.SelectionData`): container for the dropped data info (`int`): info on the target time (`int`): time of the drop """ uri_parts = urlsplit(data.get_text()) path = pathlib.Path(url2pathname(uri_parts.path) if uri_parts.scheme else uri_parts.path) if path.is_file() and path.suffix.lower() == '.pdf': self.swap_document(path.as_uri()) def unsaved_changes(self, reload=False): """ Prompt the user about what to do with changes in the document: save, discard, or cancel action Args: reload (`bool`): The unsaved changes is prompted by reloading the file Returns: `bool`: `True` iff we need to cancel the current action """ if not self.doc.has_changes(): return False dialog = Gtk.MessageDialog(title=_('Unsaved changes'), transient_for=self.p_win, type=Gtk.MessageType.WARNING) dialog.set_position(Gtk.WindowPosition.CENTER) if not reload: dialog.set_markup(_('Save changes before closing?')) dialog.format_secondary_markup(_('Unsaved changes will be lost')) dialog.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.YES) dialog.add_button(_('_Discard'), Gtk.ResponseType.NO).set_image( Gtk.Button.new_from_icon_name('edit-delete', Gtk.IconSize.BUTTON).get_image() ) dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) dialog.set_default_response(Gtk.ResponseType.YES) else: dialog.set_markup(_('The open file was modified outside of pympress but you have made unsaved changes.') + '\n' + _('Overwrite changes instead of reloading?') + '') dialog.format_secondary_markup(_('Saving changes will overwrite the changed file!') + '\n' + _('Unsaved changes will be lost.')) dialog.add_button(_('Overwrite'), Gtk.ResponseType.YES).set_image( Gtk.Button.new_from_icon_name('document-save', Gtk.IconSize.BUTTON).get_image() ) dialog.add_button(_('Reload'), Gtk.ResponseType.NO).set_image( Gtk.Button.new_from_icon_name('view-refresh', Gtk.IconSize.BUTTON).get_image() ) dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) dialog.set_default_response(Gtk.ResponseType.YES) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.YES: self.doc.save_changes() return response == Gtk.ResponseType.CANCEL or (reload and Gtk.ResponseType == Gtk.ResponseType.YES) def save_file_as(self, *args): """ Remove the current document. """ # Use a GTK file dialog to choose file dialog = Gtk.FileChooserDialog(title = _('Save as...'), transient_for = self.p_win, action = Gtk.FileChooserAction.SAVE) dialog.add_buttons(Gtk.STOCK_SAVE_AS, Gtk.ResponseType.OK) dialog.set_default_response(Gtk.ResponseType.OK) dialog.set_position(Gtk.WindowPosition.CENTER) filter = Gtk.FileFilter() filter.set_name(_('PDF files')) filter.add_mime_type('application/pdf') filter.add_pattern('*.pdf') dialog.add_filter(filter) filter = Gtk.FileFilter() filter.set_name(_('All files')) filter.add_pattern('*') dialog.add_filter(filter) response = dialog.run() if response == Gtk.ResponseType.OK: self.doc.save_changes(dialog.get_uri()) dialog.destroy() def pick_file(self, *args): """ Ask the user which file he means to open. """ # Use a GTK file dialog to choose file dialog = Gtk.FileChooserDialog(title = _('Open...'), transient_for = self.p_win, action = Gtk.FileChooserAction.OPEN) dialog.add_buttons(Gtk.STOCK_OPEN, Gtk.ResponseType.OK) dialog.set_default_response(Gtk.ResponseType.OK) dialog.set_position(Gtk.WindowPosition.CENTER) filter = Gtk.FileFilter() filter.set_name(_('PDF files')) filter.add_mime_type('application/pdf') filter.add_pattern('*.pdf') dialog.add_filter(filter) filter = Gtk.FileFilter() filter.set_name(_('All files')) filter.add_pattern('*') dialog.add_filter(filter) response = dialog.run() if response == Gtk.ResponseType.OK: self.swap_document(dialog.get_uri()) dialog.destroy() def error_opening_file(self, uri): """ Remove the current document. Args: uri (`str`): the URI of the document """ # Check if the path is valid uri_parts = urlsplit(uri) filename = pathlib.Path(url2pathname(uri_parts.path)) if uri_parts.scheme == 'file' and not filename.exists(): msg = _('Could not find the file "{}"').format(filename) else: msg = _('Error opening the file "{}"').format(uri) dialog = Gtk.MessageDialog(transient_for=self.p_win, flags=Gtk.DialogFlags.MODAL, message_type=Gtk.MessageType.ERROR, message_format=msg) dialog.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) dialog.set_position(Gtk.WindowPosition.CENTER) dialog.run() dialog.destroy() def close_file(self, *args): """ Remove the current document. """ self.swap_document(None) def save_file(self, *args): """ Remove the current document. """ self.doc.save_changes() def open_file(self, gaction, target): """ Open a document. Args: gaction (:class:`~Gio.Action`): the action triggering the call target (:class:`~GLib.Variant`): the file to open as a string variant """ self.swap_document(target.get_string()) def get_notes_mode(self): """ Simple getter. Returns: :class:`~pympress.document.PdfPage`: Truthy when we split slides in content + notes """ return self.notes_mode ############################################################################## ############################ Displaying content ############################ ############################################################################## def on_page_change(self, widget, event=None): """ Signal handler for current page editing. Args: widget (:class:`~Gtk.Widget`): the editable widget which has received the event. event (:class:`~Gdk.Event`): the GTK event. """ widget_text = widget.get_buffer().get_text() try: display_page_num = int(widget_text) except ValueError: return else: self.goto_page(display_page_num - 1) def goto_page(self, page, autoplay=False): """ Handle going to the page passed as argument Args: page (`int`): the page to which to go. Will be clipped to document pages. autoplay (`bool`): whether this page change was triggered automatically """ self.preview_page = self.doc.goto(page) if not self.page_number.editing: self.current_page = self.preview_page self.do_page_change(autoplay=autoplay) def doc_goto_prev(self, gaction=None, param=None): """ Handle going to the next page. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ self.goto_page(self.preview_page - 1) def doc_goto_next(self, gaction=None, param=None): """ Handle going to the previous page. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ if not self.page_number.editing and self.talk_time.paused: self.talk_time.unpause() else: self.goto_page(self.preview_page + 1) def doc_label_next(self, gaction=None, param=None): """ Handle going to the next page with a different label. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ self.goto_page(self.doc.label_after(self.preview_page)) def doc_label_prev(self, gaction=None, param=None): """ Handle going to the previous page with a different label. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ self.goto_page(self.doc.label_before(self.preview_page)) def doc_hist_prev(self, gaction=None, param=None): """ Handle going to the previous page in the history of visited pages Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ dest = self.doc.hist_prev() if dest is not None: self.goto_page(dest) def doc_hist_next(self, gaction=None, param=None): """ Handle going to the next page in the history of visited pages Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ dest = self.doc.hist_next() if dest is not None: self.goto_page(dest) def doc_goto_home(self, gaction=None, param=None): """ Handle going to the start of the document Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ self.goto_page(0) def doc_goto_end(self, gaction=None, param=None): """ Handle going to the end of the document Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ self.goto_page(self.doc.pages_number()) def do_page_change(self, unpause=True, autoplay=False): """ Switch to another page and display it. This is a kind of event which is supposed to be called only from the :class:`~pympress.document.Document` class. Args: is_preview (`bool`): `True` if the page change should not update the content unpause (`bool`): `True` if the page change should unpause the timer, `False` otherwise autoplay (`bool`): `True` if the page change is triggered automatically, otherwise cancel auto play """ is_preview = self.page_number.editing if not is_preview: self.preview_page = self.current_page self.app.set_action_state('page', self.current_page + 1) draw_notes = self.notes_mode draw_page = draw_notes.complement() page_content = self.doc.page(self.current_page) page_preview = self.doc.page(self.preview_page) pages_next = [self.doc.page(self.preview_page + n + 1) for n in range(self.next_frames_count)] page_notes = self.doc.notes_page(self.preview_page) # Aspect ratios and queue redraws if draw_notes: note_pr = page_notes.get_aspect_ratio(draw_notes) self.p_frame_notes.set_property('ratio', note_pr) self.p_da_notes.queue_draw() preview_pr = page_preview.get_aspect_ratio(draw_page) self.p_frame_cur.set_property('ratio', preview_pr) self.p_da_cur.queue_draw() if not is_preview: content_pr = page_content.get_aspect_ratio(draw_page) self.c_frame.set_property('ratio', content_pr) self.c_da.queue_draw() self.scribbler.scribble_p_frame.set_property('ratio', content_pr) self.scribbler.scribble_p_frame.queue_draw() next_pr = preview_pr # A default page ratio for page, frame, da in zip(pages_next, self.p_frames_next, self.p_das_next): if page is not None: next_pr = page.get_aspect_ratio(draw_page) frame.set_property('ratio', next_pr) da.queue_draw() self.annotations.load_annotations(page_preview) # Update display -- needs to be different ? self.page_number.update_page_numbers(self.preview_page, page_preview.label()) # Prerender the 4 next pages and the 2 previous ones page_max = min(self.doc.pages_number(), self.preview_page + self.next_frames_count + 4) page_min = max(0, self.preview_page - 2) for p in list(range(self.preview_page + 1, page_max)) + list(range(self.preview_page, page_min, -1)): self.cache.prerender(p) if is_preview: return # Remove scribbles and scribbling/zooming modes self.scribbler.disable_scribbling() self.scribbler.page_change(self.preview_page, page_preview.label()) self.zoom.stop_zooming() # Update medias self.medias.replace_media_overlays(self.doc.page(self.current_page), draw_page) # Start counter if needed if unpause: self.talk_time.unpause() if not autoplay: self.autoplay.stop_looping() self.timing.transition(self.preview_page, self.talk_time.current_time()) def on_draw(self, widget, cairo_context): """ Manage draw events for both windows. This callback may be called either directly on a page change or as an event handler by GTK. In both cases, it determines which widget needs to be updated, and updates it, using the :class:`~pympress.surfacecache.SurfaceCache` if possible. Args: widget (:class:`~Gtk.Widget`): the widget to update cairo_context (:class:`~cairo.Context`): the Cairo context (or `None` if called directly) """ if widget is self.c_da: # Current page if self.blanked: return page = self.doc.page(self.current_page) elif widget is self.p_da_cur or widget is self.scribbler.scribble_p_da: # Current page 'preview' page = self.doc.page(self.preview_page) elif widget is self.p_da_notes: # Notes page, aligned with preview page = self.doc.notes_page(self.preview_page) elif widget in self.p_das_next: offset = 1 + int(widget.get_name()[len('p_da_next'):]) page = self.doc.page(self.preview_page + offset) # No next page: just return so we won't draw anything if page is None: return else: logger.warning(_('Unknown widget "{}" to draw'.format(widget.getname()))) return if not page.can_render(): return name = widget.get_name().rstrip('0123456789') nb = page.number() wtype = self.cache.get_widget_type(name) ww, wh = widget.get_allocated_width(), widget.get_allocated_height() window = widget.get_window() scale = window.get_scale_factor() if self.zoom.scale != 1. and (widget is self.p_da_cur or widget is self.c_da or widget is self.scribbler.scribble_p_da): zoom_matrix = self.zoom.get_matrix(ww, wh) name += '_zoomed' else: zoom_matrix = cairo.Matrix() pb = self.cache.get(name, nb) if pb is None: if self.resize_panes and widget in self.p_das_next + [self.p_da_cur, self.p_da_notes]: # too slow to render here when resize_panes things return # Cache miss: render the page, and save it to the cache pb = window.create_similar_image_surface(cairo.Format.RGB24, ww * scale, wh * scale, scale) cairo_prerender = cairo.Context(pb) cairo_prerender.transform(zoom_matrix) page.render_cairo(cairo_prerender, ww, wh, wtype) self.cache.set(name, nb, pb) cairo_context.set_source_surface(pb, 0, 0) cairo_context.paint() else: # Cache hit: draw the surface from the cache to the widget cairo_context.set_source_surface(pb, 0, 0) cairo_context.paint() if widget is self.c_da or widget is self.p_da_cur or widget is self.scribbler.scribble_p_da: cairo_context.save() cairo_context.transform(zoom_matrix) self.scribbler.draw_scribble(widget, cairo_context) self.zoom.draw_zoom_target(widget, cairo_context) cairo_context.restore() if widget is self.c_da or widget is self.p_da_cur or widget is self.scribbler.scribble_p_da: # do not use the zoom matrix for the pointer, it is relative to the screen not the slide self.laser.render_pointer(cairo_context, ww, wh) def clear_zoom_cache(self): """ Callback to clear the cache of zoomed widgets. """ self.cache.clear_cache(self.c_da.get_name() + '_zoomed') self.cache.clear_cache(self.p_da_cur.get_name() + '_zoomed') self.cache.clear_cache(self.scribbler.scribble_p_da.get_name() + '_zoomed') def redraw_current_slide(self): """ Callback to queue a redraw of the current slides (in both winows). """ self.c_da.queue_draw() self.p_da_cur.queue_draw() self.scribbler.scribble_p_da.queue_draw() ############################################################################## ############################ User inputs ############################ ############################################################################## def on_navigation(self, widget, event): """ Manage key presses for both windows. Args: widget (:class:`~Gtk.Widget`): the widget in which the event occurred (ignored) event (:class:`~Gdk.Event`): the event that occurred Returns: `bool`: whether the event was consumed """ if event.type != Gdk.EventType.KEY_PRESS: return False # Try passing events to special-behaviour widgets (spinner, ett, zooming, scribbler) in case they are enabled if self.page_number.on_keypress(widget, event): return True elif self.est_time.on_keypress(widget, event): return True return False def validate_current_input(self, gaction, param=None): """ Handle the action validating the input, if applicable. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ if self.page_number.try_validate(): return True elif self.est_time.try_validate(): return True return False def on_key_input(self, widget, event): """ Handle key strokes at top level, only for when editing needs to bypass action accelerators Args: widget (:class:`~Gtk.Widget`): the widget which has received the key stroke event (:class:`~Gdk.Event`): the GTK event, which contains the key stroke details """ if self.annotations.key_event(widget, event): return True return False def cancel_current_input(self, gaction, param=None): """ Handle the action cancelling the input, if applicable. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None """ if self.page_number.try_cancel(): return True elif self.est_time.try_cancel(): return True elif self.zoom.try_cancel(): return True elif self.scribbler.try_cancel(): return True elif self.annotations.try_cancel(): return True elif self.autoplay.stop_looping(): return True return False def on_scroll(self, widget, event): """ Manage scroll events. Args: widget (:class:`~Gtk.Widget`): the widget in which the event occurred (ignored) event (:class:`~Gdk.Event`): the event that occurred Returns: `bool`: whether the event was consumed """ if event.type != Gdk.EventType.SCROLL: return False # send to spinner if it is active elif self.page_number.on_scroll(widget, event): return True else: return False def track_motions(self, widget, event): """ Track mouse motion events. Handles mouse motions on the "about" menu. Args: widget (:class:`~Gtk.Widget`): the widget that received the mouse motion event (:class:`~Gdk.Event`): the GTK event containing the mouse position Returns: `bool`: whether the event was consumed """ if self.zoom.track_zoom_target(widget, event): return True elif self.scribbler.track_scribble(widget, event): return True elif self.laser.track_pointer(widget, event): return True else: return self.hover_link(widget, event) def track_clicks(self, widget, event): """ Track mouse press and release events. Handles clicks on the slides. Args: widget (:class:`~Gtk.Widget`): the widget that received the click event (:class:`~Gdk.Event`): the GTK event containing the click position Returns: `bool`: whether the event was consumed """ if self.zoom.toggle_zoom_target(widget, event): return True elif self.scribbler.toggle_scribble(widget, event): return True elif self.laser.toggle_pointer(widget, event): return True else: return self.click_link(widget, event) def click_link(self, widget, event): """ Check whether a link was clicked and follow it. Handles a click on a slide. Args: widget (:class:`~Gtk.Widget`): the widget in which the event occurred event (:class:`~Gdk.Event`): the event that occurred Returns: `bool`: whether the event was consumed """ if event.type == Gdk.EventType.BUTTON_RELEASE: return False # Where did the event occur? if widget in self.p_das_next: offset = 1 + int(widget.get_name()[len('p_da_next'):]) page = self.doc.page(self.preview_page + offset) elif widget is self.p_da_notes: page = self.doc.notes_page(self.preview_page) elif widget is self.p_da_cur: page = self.doc.page(self.preview_page) else: page = self.doc.page(self.current_page) if page is None: return False x, y = self.zoom.get_slide_point(widget, event) page_mode = self.notes_mode if widget is self.p_da_notes else self.notes_mode.complement() link = page.get_link_at(x, y, page_mode) if event.type == Gdk.EventType.BUTTON_PRESS and link is not None: link.follow() return True else: return False def hover_link(self, widget, event): """ Manage events related to hyperlinks, setting the cursor to a pointer if the hovered region is clickable. Args: widget (:class:`~Gtk.Widget`): the widget in which the event occurred event (:class:`~Gdk.Event`): the event that occurred Returns: `bool`: whether the event was consumed """ if event.type != Gdk.EventType.MOTION_NOTIFY: return False # Where did the event occur? if widget in self.p_das_next: offset = 1 + int(widget.get_name()[len('p_da_next'):]) page = self.doc.page(self.preview_page + offset) elif widget is self.p_da_notes: page = self.doc.notes_page(self.preview_page) elif widget is self.p_da_cur: page = self.doc.page(self.preview_page) else: page = self.doc.page(self.current_page) if page is None: return False x, y = self.zoom.get_slide_point(widget, event) page_mode = self.notes_mode if widget is self.p_da_notes else self.notes_mode.complement() if page.get_link_at(x, y, page_mode): extras.Cursor.set_cursor(widget, 'pointer') return False else: extras.Cursor.set_cursor(widget, 'parent') return True def switch_fullscreen(self, gaction, target): """ Switch the Content window to fullscreen (if in normal mode) or to normal mode (if fullscreen). Screensaver will be disabled when entering fullscreen mode, and enabled when leaving fullscreen mode. Args: widget (:class:`~Gtk.Widget`): the widget in which the event occurred Returns: `bool`: whether some window's full screen status got toggled """ if gaction.get_name() == 'content-fullscreen': widget = self.c_win elif gaction.get_name() == 'presenter-fullscreen': widget = self.p_win else: raise ValueError('Do not know which widget to put full screen') if widget != self.c_win and widget != self.p_win: logger.error(_("Unknow widget {} to be fullscreened, aborting.").format(widget)) return False toggle_to = not gaction.get_state().get_boolean() window = widget.get_window() cur_state = (window.get_state() & Gdk.WindowState.FULLSCREEN) if window is not None else False if cur_state == toggle_to: return False elif cur_state: widget.unfullscreen() else: widget.fullscreen() if gaction.get_name() == 'content-fullscreen': self.set_screensaver(disabled=toggle_to) gaction.change_state(GLib.Variant.new_boolean(toggle_to)) return True def set_screensaver(self, disabled): """ Disable or re-enable the screensaver. Args: disabled (`bool`): `True` iff the screensaver should be disabled, otherwise enabled. """ if not disabled: if self.inhibit_cookie: self.app.uninhibit(self.inhibit_cookie) elif self.inhibit_cookie is not None: util.hard_set_screensaver(disabled=False) self.inhibit_cookie = None else: flags = (Gtk.ApplicationInhibitFlags.LOGOUT | Gtk.ApplicationInhibitFlags.SWITCH | Gtk.ApplicationInhibitFlags.SUSPEND | Gtk.ApplicationInhibitFlags.IDLE) self.inhibit_cookie = self.app.inhibit(self.c_win, flags, _("Fullscreen Presentation running")) if not self.inhibit_cookie: logger.warning(_('Gtk.Application.inhibit failed preventing screensaver, trying hard disabling')) util.hard_set_screensaver(disabled=True) def update_frame_position(self, widget, user_data): """ Callback to preview the frame alignment, called from the Gtk.SpinButton. Args: widget (:class:`~Gtk.SpinButton`): The button updating the slide alignment in the drawing area widget user_data (`str`): The property being set, either the x or y alignment (resp. xalign and yalign). """ self.c_frame.set_property(user_data, widget.get_value()) def adjust_frame_position(self, *args): """ Select how to align the frame on screen. """ win_aspect_ratio = float(self.c_win.get_allocated_width()) / self.c_win.get_allocated_height() if win_aspect_ratio <= float(self.c_frame.get_property("ratio")): prop = "yalign" else: prop = "xalign" val = self.c_frame.get_property(prop) button = Gtk.SpinButton() button.set_adjustment(Gtk.Adjustment(lower=0.0, upper=1.0, step_increment=0.01)) button.set_digits(2) button.set_value(val) button.connect("value-changed", self.update_frame_position, prop) popup = Gtk.Dialog(title = _("Adjust alignment of slides in projector screen"), transient_for = self.p_win) popup.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK) box = popup.get_content_area() box.add(button) popup.show_all() response = popup.run() popup.destroy() # revert if we cancelled if response == Gtk.ResponseType.CANCEL: self.c_frame.set_property(prop, val) else: self.config.set('content', prop, str(button.get_value())) ############################################################################## ############################ Option toggles ############################ ############################################################################## def swap_screens(self, *args): """ Swap the monitors on which each window is displayed (if there are 2 monitors at least). """ screen = self.p_win.get_screen() # Though Gtk.Window is a Gtk.Widget get_parent_window() actually returns None on self.{c,p}_win p_monitor = screen.get_monitor_at_point(*self.p_win.get_position()) c_monitor = screen.get_monitor_at_point(*self.c_win.get_position()) if screen.get_n_monitors() == 1 or p_monitor == c_monitor: return self.move_window(screen, self.c_win, c_monitor, p_monitor) self.move_window(screen, self.p_win, p_monitor, c_monitor) def switch_blanked(self, gaction, param): """ Switch the blanked mode of the content screen. Args: gaction (:class:`~Gio.Action`): the action triggering the call param (:class:`~GLib.Variant`): the parameter as a variant, or None Returns: `bool`: whether the notes blanking has been toggled. """ self.blanked = not self.blanked self.c_da.queue_draw() gaction.change_state(GLib.Variant.new_boolean(self.blanked)) return True def layout_name(self, notes_mode): """ Return the layout made for the selected notes_mode Args: notes_mode (:class:`~pympress.document.PdfPage`): the mode/positioning of notes Returns: `str`: a string representing the appropriate layout """ if self.scribbler.scribbling_mode: return 'highlight' elif notes_mode.direction() == 'page number': return 'note_pages' elif notes_mode: return 'notes' else: return 'plain' def load_layout(self, new): """ Replace the current layout Args: new (`str`): the name of the layout to load, `None` to use current layout automatically """ if new is None: new = self.layout_name(self.notes_mode) else: self.layout_editor.set_current_layout(new) pane_handles = self.replace_layout(self.config.get_layout(new), self.p_central, self.placeable_widgets, self.on_pane_event) self.scribbler.adjust_tools_orientation() self.pane_handle_pos.update(pane_handles) # queue visibility of all newly added widgets, make sure visibility is right self.p_central.show_all() self.p_frame_annot.set_visible(self.show_annotations) def change_notes_pos(self, gaction, target, force=False): """ Switch the position of the nodes in the slide. Returns: gaction (:class:`~Gio.Action`): the action triggering the call target (:class:`~GLib.Variant`): the notes position as a string variant force (`bool`): Whether to force the notes switch even if it’s already enabled Returns: `bool`: whether the notes position has been toggled """ target_mode = document.PdfPage[target.get_string().upper()] # Redundant toggle, do nothing if target_mode == self.chosen_notes_mode: return False # Update the choice, except for NONE or BEFORE/AFTER if target_mode: self.chosen_notes_mode = target_mode gaction.change_state(target) self.config.set('notes position', target_mode.direction(), target_mode.name.lower()) # Change the notes arrangement if they are enabled or if we are forced to if self.notes_mode or force: self.switch_mode(self.app.lookup_action('notes-mode'), target_mode=target_mode, force=True) return True def switch_mode(self, gaction, target_mode=None, force=False): """ Switch the display mode to "Notes mode" or "Normal mode" (without notes). Returns: gaction (:class:`~Gio.Action`): the action triggering the call target_mode (:class:`~pympress.document.PdfPage`): the mode to which we should switch force (`bool`): Whether to force the mode switch even if it’s already enabled Returns: `bool`: whether the notes mode has been toggled """ if target_mode is None: target_mode = document.PdfPage.NONE if self.notes_mode else self.chosen_notes_mode if target_mode == self.notes_mode and not force: return False self.scribbler.disable_scribbling() self.doc.set_notes_after(target_mode.direction() == 'page number') self.load_layout(self.layout_name(target_mode)) self.notes_mode = target_mode page_type = self.notes_mode.complement() self.cache.set_widget_type('c_da', page_type) self.cache.set_widget_type('c_da_zoomed', page_type) self.cache.set_widget_type('p_da_next', page_type) self.cache.set_widget_type('p_da_cur', page_type) self.cache.set_widget_type('p_da_cur_zoomed', page_type) self.cache.set_widget_type('scribble_p_da', page_type) self.cache.set_widget_type('p_da_notes', self.notes_mode) if self.notes_mode: self.cache.enable_prerender('p_da_notes') self.cache.disable_prerender('p_da_cur') else: self.cache.disable_prerender('p_da_notes') self.cache.enable_prerender('p_da_cur') self.medias.adjust_margins_for_mode(page_type) self.do_page_change(unpause=False) self.page_number.set_last(self.doc.pages_number()) self.app.set_action_state('notes-mode', bool(self.notes_mode)) return True def switch_annotations(self, gaction, target): """ Switch the display to show annotations or to hide them. Returns: gaction (:class:`~Gio.Action`): the action triggering the call target (:class:`~GLib.Variant`): the parameter as a variant, or None Returns: `bool`: whether the mode has been toggled. """ self.show_annotations = not self.show_annotations self.p_frame_annot.set_visible(self.show_annotations) self.config.set('presenter', 'show_annotations', 'on' if self.show_annotations else 'off') if self.show_annotations: parent = self.p_frame_annot.get_parent() if issubclass(type(parent), Gtk.Paned): if parent.get_orientation() == Gtk.Orientation.HORIZONTAL: size = parent.get_allocated_width() else: size = parent.get_allocated_height() parent.set_position(self.pane_handle_pos[parent] * size) self.annotations.load_annotations(self.doc.page(self.preview_page)) gaction.change_state(GLib.Variant.new_boolean(self.show_annotations)) return True def switch_bigbuttons(self, *args): """ Toggle the display of big buttons (nice for touch screens). """ self.show_bigbuttons = not self.show_bigbuttons if self.show_bigbuttons: # potentially reduce font self.adjust_bottom_bar_font() self.prev_button.set_visible(self.show_bigbuttons) self.next_button.set_visible(self.show_bigbuttons) self.laser_button.set_visible(self.show_bigbuttons) self.highlight_button.set_visible(self.show_bigbuttons) if not self.show_bigbuttons: # potentially increase font self.adjust_bottom_bar_font() self.config.set('presenter', 'show_bigbuttons', 'on' if self.show_bigbuttons else 'off') self.app.set_action_state('big-buttons', self.show_bigbuttons) ## # Local Variables: # mode: python # indent-tabs-mode: nil # py-indent-offset: 4 # fill-column: 80 # end: pympress-1.7.1/pympress/util.py000066400000000000000000000221611415371354200166220ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # util.py # # Copyright 2009, 2010 Thomas Jost # Copyright 2015 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ :mod:`pympress.util` -- various utility functions ------------------------------------------------- """ import logging logger = logging.getLogger(__name__) import subprocess import importlib import os import sys import pathlib if not getattr(sys, 'frozen', False): # doesn’t play too well with cx_Freeze import pkg_resources IS_POSIX = os.name == 'posix' IS_MAC_OS = sys.platform == 'darwin' IS_WINDOWS = os.name == 'nt' if IS_WINDOWS: try: import winreg except ImportError: import _winreg as winreg def get_pympress_meta(): """ Get metadata (version, etc) from pympress' __init__.py or git describe. Returns: `dict`: metadata properties (version, contributors) mapped to their values """ module = importlib.import_module('pympress.__init__') info = {'version': module.__version__, 'contributors': module.__author__} if getattr(sys, 'frozen', False): return info # Try and get a git describe output in case we are on a dirty/editable version try: path = pkg_resources.get_distribution('pympress').module_path command = 'git --git-dir={}/.git describe --tags --long --dirty'.split() command[1] = command[1].format(path) # after spliting in case path has whitespace git_version = subprocess.check_output(command, stderr = subprocess.DEVNULL) # answer format is: {last tag}-{commit count since tag}-g{commit sha1 hash}[-dirty] tag, count, sha, dirty = (git_version + '-').decode('utf-8').strip().split('-', 3) if count != '0' or dirty: info['version'] = '{}+{}@{}'.format(tag.lstrip('v'), count, sha.lstrip('g')) except (pkg_resources.DistributionNotFound, subprocess.CalledProcessError): logger.debug('Failed to get git describe output', exc_info = True) finally: return info def __get_resource_path(*path_parts): """ Return the resource path based on whether its frozen or not. Paths parts given should be relative to the pympress package dir. Args: name (`tuple` of `str`): The directories and filename that constitute the path to the resource, relative to the pympress distribution Returns: :class:`~pathlib.Path`: The path to the resource """ if getattr(sys, 'frozen', False): return pathlib.Path(sys.executable).parent.joinpath(*path_parts) else: req = pkg_resources.Requirement.parse('pympress') return pathlib.Path(pkg_resources.resource_filename(req, '/'.join(('pympress',) + path_parts))) def get_locale_dir(): """ Returns the path to the locale directory. Returns: :class:`~pathlib.Path`: The path to the locale directory """ return __get_resource_path('share', 'locale') def get_portable_config(): """ Returns the path to the configuration file for a portable install (i.e. in the install root). Returns: :class:`~pathlib.Path`: The path to the portable configuration file. """ return __get_resource_path('pympress.conf') def get_default_config(): """ Returns the path to the configuration file containing the defaults. Returns: :class:`~pathlib.Path`: The path to the portable configuration file. """ return __get_resource_path('share', 'defaults.conf') def get_user_config(): """ Returns the path to the configuration file in the user config directory Returns: :class:`~pathlib.Path`: path to the user configuration file. """ if IS_WINDOWS: base_dir = pathlib.Path(os.getenv('APPDATA')) elif IS_MAC_OS: base_dir = pathlib.Path('~/Library/Preferences').expanduser() else: base_dir = pathlib.Path(os.getenv('XDG_CONFIG_HOME', '~/.config')).expanduser() if not base_dir.exists(): base_dir.mkdir(parents=True) return base_dir.joinpath('pympress' + ('.ini' if IS_WINDOWS else '')) def load_style_provider(style_provider): """ Load the css and in a style provider Args: style_provider (:class:`~Gtk.CssProvider`): The style provider in which to load CSS Returns: :class:`~Gtk.CssProvider`: The style provider with CSS loaded """ style_provider.load_from_path(str(__get_resource_path('share', 'css', 'default.css'))) return style_provider def get_icon_path(name): """ Get the path for an image from pympress' resources Args: name (`str`): The name of the icon to load Returns: `str`: The path to the icon to load """ return str(__get_resource_path('share', 'pixmaps', name)) def get_ui_resource_file(name, ext='.glade'): """ Load an UI definition file from pympress' resources Args: name (`str`): The name of the UI to load ext (`str`): The extension of the file Returns: `str`: The full path to the glade file """ return str(__get_resource_path('share', 'xml', name + ext)) def list_icons(): """ List the icons from pympress' resources. Returns: `list` of `str`: The paths to the icons in the pixmaps directory """ return list(map(str, __get_resource_path('share', 'pixmaps').glob('pympress-*.png'))) def get_log_path(): """ Returns the appropriate path to the log file in the user app dirs. Returns: :class:`~pathlib.Path`: path to the log file. """ if IS_WINDOWS: base_dir = pathlib.Path(os.getenv('LOCALAPPDATA', os.getenv('APPDATA'))) elif IS_MAC_OS: base_dir = pathlib.Path('~/Library/Logs').expanduser() else: base_dir = pathlib.Path(os.getenv('XDG_CACHE_HOME', '~/.cache')).expanduser() if not base_dir.exists(): base_dir.mkdir(parents=True) return base_dir.joinpath('pympress.log') def fileopen(f): """ Call the right function to open files, based on the platform. Args: f (path-like): path to the file to open """ if IS_WINDOWS: os.startfile(f) elif IS_MAC_OS: subprocess.call(['open', str(f)]) else: subprocess.call(['xdg-open', str(f)]) def hard_set_screensaver(disabled): """ Enable or disable the screensaver. Args: disabled (`bool`): if `True`, indicates that the screensaver must be disabled; otherwise it will be enabled """ if IS_MAC_OS: # On Mac OS X we can use caffeinate to prevent the display from sleeping if disabled: if hard_set_screensaver.caffeinate_process is None or hard_set_screensaver.caffeinate_process.poll(): hard_set_screensaver.caffeinate_process = subprocess.Popen(['caffeinate', '-d', '-w', str(os.getpid())]) else: if hard_set_screensaver.caffeinate_process and not hard_set_screensaver.caffeinate_process.poll(): hard_set_screensaver.caffeinate_process.kill() hard_set_screensaver.caffeinate_process.poll() hard_set_screensaver.caffeinate_process = None elif IS_WINDOWS: try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Control Panel\Desktop', 0, winreg.KEY_QUERY_VALUE | winreg.KEY_SET_VALUE) as key: if disabled: value, regtype = winreg.QueryValueEx(key, "ScreenSaveActive") assert(regtype == winreg.REG_SZ) hard_set_screensaver.dpms_was_enabled = (value == "1") if hard_set_screensaver.dpms_was_enabled: winreg.SetValueEx(key, "ScreenSaveActive", 0, winreg.REG_SZ, "0") elif hard_set_screensaver.dpms_was_enabled: winreg.SetValueEx(key, "ScreenSaveActive", 0, winreg.REG_SZ, "1") except (OSError, PermissionError): logger.exception(_("access denied when trying to access screen saver settings in registry!")) elif IS_POSIX: logger.warning(_("Should not require hard enable/disable screensaver on Linux")) else: logger.warning(_("Unsupported OS: can't enable/disable screensaver")) #: remember DPMS setting before we change it hard_set_screensaver.dpms_was_enabled = None #: A :class:`~subprocess.Popen` object to track the child caffeinate process hard_set_screensaver.caffeinate_process = None ## # Local Variables: # mode: python # indent-tabs-mode: nil # py-indent-offset: 4 # fill-column: 80 # end: pympress-1.7.1/scripts/000077500000000000000000000000001415371354200150765ustar00rootroot00000000000000pympress-1.7.1/scripts/poedit.sh000077500000000000000000000051361415371354200167260ustar00rootroot00000000000000#!/usr/bin/env bash cd `git rev-parse --show-toplevel || readlink -f "$0" | xargs dirname | xargs dirname` pot=pympress/share/locale/pympress.pot upload() { printf 'Uploading new strings to poeditor: ' curl -sX POST https://api.poeditor.com/v2/projects/upload \ -F api_token="$poeditor_api_token" \ -F id="301055" -F updating="terms" -F file=@"$pot" \ -F tags="{\"obsolete\":\"removed\"}" \ | jq -r '.response.message' } languages() { curl -sX POST https://api.poeditor.com/v2/languages/list \ -F api_token="$poeditor_api_token" \ -F id="301055" | jq -r 'select(.response.code == "200") | .result.languages[] | select(.percentage > 5) | .code' } contributors() { curl -sX POST https://api.poeditor.com/v2/contributors/list \ -F api_token="$poeditor_api_token" \ -F id="301055" | jq --arg lang "$*" -r 'select(.response.code == "200") | .result.contributors[] | select(IN(.permissions[].languages[]; $lang | split(" ")[])) | .name' | while read name; do # hold "name,", hold & delete any line matching name, at the last translator insert the hold space sed -e "1{h;s/.*/${name},/;x}" -e "/\<${name}\>/{h;d}" -e '//{x;G}' -i README.md done } download() { lang=$1 printf "Updating %s:\n" "$lang" url=`curl -sX POST https://api.poeditor.com/v2/projects/export \ -F api_token="$poeditor_api_token" \ -F id="301055" -F language="$lang" -F type="po" \ | jq -r 'select(.response.code == "200") | .result.url'` test -n "$url" && curl -so "pympress/share/locale/${lang}/LC_MESSAGES/pympress.po" "$url" } getpass() { if test -z "$poeditor_api_token"; then poeditor_api_token=`$SSH_ASKPASS "Password for 'https://api.poeditor.com/projects/v2/': "` fi } if [ $# -eq 0 ]; then echo "Usage: $0 " echo "Where command is one of: upload, languages, download, contributors" echo "requires curl and jq" fi while [ $# -gt 0 ]; do if test "$1" = "upload"; then getpass upload elif test "$1" = "languages"; then getpass languages elif test "$1" = "download"; then getpass avail_lang=`languages` for lang in $avail_lang; do download $lang done contributors $avail_lang elif test "$1" = "contributors"; then getpass avail_lang=`languages` contributors $avail_lang else echo "Unrecognised command $1 use one of: upload, languages, download, contributors" exit 1 fi shift done pympress-1.7.1/setup.cfg000066400000000000000000000126601415371354200152350ustar00rootroot00000000000000[metadata] name = pympress version = attr: pympress.__version__ keywords = pdf-viewer, beamer, presenter, slide, projector, pdf-reader, presentation, python, poppler, gtk, pygi, vlc description = A simple and powerful dual-screen PDF reader designed for presentations long_description = file: README.md long_description_content_type = text/markdown author = Cimbali, Thomas Jost, Christof Rath, Epithumia author_email = me@cimba.li url = https://github.com/Cimbali/pympress/ download_url = https://github.com/Cimbali/pympress/releases/latest project_urls = Issues = https://github.com/Cimbali/pympress/issues/ Documentation = https://cimbali.github.io/pympress/ Source Code = https://github.com/Cimbali/pympress/ license = GPL-2.0-or-later license_files = LICENSE.txt classifiers = Development Status :: 5 - Production/Stable Environment :: X11 Applications :: GTK Intended Audience :: Education Intended Audience :: End Users/Desktop Intended Audience :: Information Technology Intended Audience :: Science/Research License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) Natural Language :: English Natural Language :: French Natural Language :: German Natural Language :: Polish Natural Language :: Spanish Natural Language :: Czech Operating System :: OS Independent Programming Language :: Python Topic :: Multimedia :: Graphics :: Presentation Topic :: Multimedia :: Graphics :: Viewers [options] packages = pympress pympress.media_overlays python_requires = >=3.4 install_requires = watchdog build_requires = setuptools babel [options.extras_require] build_sphinx = docutils==0.16 sphinx recommonmark sphinxcontrib-napoleon sphinx-rtd-theme babel = babel babelgladeextractor vlc_video = python-vlc [options.package_data] pympress = share/defaults.conf share/xml/*.glade share/xml/*.xml share/css/*.css share/pixmaps/*.png share/locale/*/LC_MESSAGES/pympress.mo [options.entry_points] gui_scripts = pympress = pympress.__main__:main [style] based_on_style = pep8 column_limit = 120 split_complex_comprehension = on split_penalty_comprehension = 5000 split_penalty_excess_character = 40 use_tabs = off indent_width = 4 [flake8] docstring-convention = google max-line-length = 120 builtins = _, unicode exclude = .git .eggs __pycache__ build/ dist/ ignore = D107, D200, D210, D413, E251, E302, E303, W504, # never complain about those D205, D212, D415, E201, E221, E241, E266, E301, E402, E701, E731 # allow sometimes, e.g. aligning code etc. per-file-ignores = # do not complain about dummy functions pympress/media_overlays/gif_backend.py: D102, E704 pympress/__main__.py: F401 [build_sphinx] source-dir = docs config-dir = docs builder = html [extract_messages] no_location = true no_wrap = true sort_output = true omit_header = true output-file = pympress/share/locale/pympress.pot mapping_file = pympress/share/locale/babel_mapping.cfg [compile_catalog] domain = pympress directory = pympress/share/locale/ use-fuzzy = false statistics = true [pysrpm] flavour = pympress extract_dependencies = no requires = gobject-introspection %%{py3_dist watchdog} typelib_deps = typelib(cairo) typelib(GLib) typelib(DBus) typelib(DBusGLib) typelib(GObject) typelib(Gdk) typelib(GdkPixbuf) typelib(Gio) typelib(Gtk) typelib(Poppler) typelib(Gst) typelib(GstAllocators) typelib(GstApp) typelib(GstAudio) typelib(GstVideo) typelib(GstGL) typelib_recommends = typelib(GstMpegts) typelib(GstWebRTC) typelib(GstBadAudio) typelib(GstCodecs) requires_suse = gtk3 libpoppler-glib8 libgdk_pixbuf-2_0-0 gstreamer gstreamer-plugins-base gstreamer-plugins-good gstreamer-plugins-good-gtk libgstvideo-1_0-0 recommends_suse = gstreamer-plugins-ugly gstreamer-plugins-bad requires_mandriva-mga = gtk+3.0 (libpoppler-glib8 or lib64poppler-glib8) (libgdk_pixbuf2.0 or libgdk_pixbuf2.0_0) libgstreamer1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good recommends_mandriva-mga = gstreamer1.0-plugins-ugly gstreamer1.0-plugins-bad requires_fedora-centos = gtk3 poppler-glib gdk-pixbuf2 gstreamer1 gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-plugins-good-gtk gstreamer1-plugins-bad-free gstreamer1-plugins-ugly-free recommends_fedora-centos = gstreamer1-plugins-good-extras gstreamer1-plugins-bad-free-extras gstreamer1-plugins-ugly gstreamer1-plugins-bad-free [pysrpm.pympress] preamble = %%define normalize() %%(echo %%* | tr "[:upper:]_ " "[:lower:]--") %%{{?!py3_dist:%%define py3_dist() (python%%{{python3_version}}dist(%%{{normalize %%1}}) or python3-%%1)}} ${base:preamble} %%if %%{{?!rhel:8}}%%{{?rhel}} >= 8 Requires: (%%{{py3_dist pygobject}} or python3-gobject) %%else Requires: python3%%{{suffix:%%{{python3_version}}}}-gobject %%endif %%if 0%%{{?suse_version}} Requires: ${requires_suse} Recommends: ${recommends_suse} %%endif %%if 0%%{{?mdkversion}}%%{{?mga_version}} Requires: ${requires_mandriva-mga} Recommends: ${recommends_mandriva-mga} %%endif %%if 0%%{{?fedora}}%%{{?centos_version}}%%{{?scientificlinux_version}}%%{{?rhel}} Requires: ${requires_fedora-centos} %%if %%{{?!rhel:8}}%%{{?rhel}} >= 8 Recommends: ${recommends_fedora-centos} %%endif %%endif %%if 0%%{{?suse_version}}%%{{?mga_version}}%%{{?mdkversion}} Requires: ${typelib_deps} Recommends: ${typelib_recommends} %%endif post = ${base:post} if [ $$1 -gt 1 ]; then # On update, remove directories incorrectly left behind by previous versions find "%%{{python3_sitelib}}" -maxdepth 1 -name 'pympress-1.5.*' -print0 | xargs -0 --no-run-if-empty rm -r fi pympress-1.7.1/setup.py000077500000000000000000000260451415371354200151330ustar00rootroot00000000000000#!/usr/bin/env python3 # # setup.py # # Copyright 2009 Thomas Jost # Copyright 2015 Cimbali # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. """ pympress setup script. Mostly wrapping logic for freezing (with cx_Freeze for windows builds). All configuration is in setup.cfg. """ import os import re import sys import pathlib import subprocess from ctypes.util import find_library import setuptools from distutils.cmd import Command from distutils.errors import DistutilsOptionError from setuptools.command.build_py import build_py def find_index_startstring(haystack, needle, start=0, stop=sys.maxsize): """ Return the index of the first string in haystack starting with needle, or raise ValueError if none match. """ try: return next(n for n, v in enumerate(haystack[start:stop], start) if v.startswith(needle)) except StopIteration: raise ValueError('No string starts with ' + needle) class GettextBuildCatalog(Command): """ Patched build command to generate translations .mo files using gettext’s msgfmt This is used for build systems that do not have easy access to Babel """ user_options = [ ('domain=', 'D', "domains of PO files (space separated list, default 'messages')"), ('directory=', 'd', 'path to base directory containing the catalogs'), ('use-fuzzy', 'f', 'also include fuzzy translations'), ('statistics', None, 'print statistics about translations') ] def initialize_options(self): """ Initialize options """ self.domain = None self.directory = None self.use_fuzzy = False self.statistics = True def finalize_options(self): """ Finalize options """ assert self.domain is not None and self.directory is not None def run(self): """ Run msgfmt before running (parent) develop command """ po_wildcard = pathlib.Path(self.directory).glob(str(pathlib.Path('*', 'LC_MESSAGES', self.domain + '.po'))) for po in po_wildcard: print(po) mo = po.with_suffix('.mo') cmd = ['msgfmt', str(po), '-o', str(mo)] if self.use_fuzzy: cmd.insert(1, '--use-fuzzy') if self.statistics: cmd.insert(1, '--statistics') subprocess.check_output(cmd) class BuildWithCatalogs(build_py): """ Patched build command to generate translations .mo files using Babel This is what we use by default, e.g. when distributing through PyPI """ def run(self): """ Run compile_catalog before running (parent) develop command """ try: self.distribution.run_command('compile_catalog') except DistutilsOptionError as err: if err.args == ('no message catalogs found',): pass # Running from a source tarball − compiling already done else: raise build_py.run(self) # All functions listing resources return a list of pairs: (system path, distribution relative path) def gtk_resources(): """ Returns a list of the non-DLL Gtk resources to include in a frozen/binary package. """ include_path = pathlib.Path(find_library('libgtk-3-0')).parent include_path = include_path.parent if include_path.name in {'bin', 'lib', 'lib64'} else include_path include_files = [] resources = [ pathlib.Path('etc'), pathlib.Path('lib', 'girepository-1.0'), pathlib.Path('lib', 'gtk-3.0'), pathlib.Path('lib', 'gdk-pixbuf-2.0'), pathlib.Path('share', 'poppler'), pathlib.Path('share', 'themes'), pathlib.Path('share', 'icons'), pathlib.Path('share', 'glib-2.0'), pathlib.Path('share', 'xml') ] for f in resources: p = include_path.joinpath(f) if p.exists(): include_files.append((str(p), str(f))) else: print('WARNING: Can not find {} (at {})'.format(f, p)) return include_files def dlls(): """ Returns a list of all DLL files we need to include, in a frozen/binary package on windows. Relies on a hardcoded list tested for the appveyor build setup. """ if os.name != 'nt': return [] libs = 'libatk-1.0-0.dll libbrotlicommon.dll libbrotlidec.dll libcurl-4.dll libdatrie-1.dll \ libepoxy-0.dll libfribidi-0.dll libgdk-3-0.dll libgdk_pixbuf-2.0-0.dll libgif-7.dll \ libgio-2.0-0.dll libgirepository-1.0-1.dll libglib-2.0-0.dll libgobject-2.0-0.dll libgtk-3-0.dll \ libidn2-0.dll libjpeg-8.dll liblcms2-2.dll libnghttp2-14.dll libnspr4.dll libopenjp2-7.dll \ libpango-1.0-0.dll libpangocairo-1.0-0.dll libpangoft2-1.0-0.dll libpangowin32-1.0-0.dll \ libplc4.dll libplds4.dll libpoppler-105.dll libpoppler-cpp-0.dll libpoppler-glib-8.dll libpsl-5.dll \ libpython{0.major}.{0.minor}.dll libstdc++-6.dll libthai-0.dll libtiff-5.dll libunistring-2.dll \ libwinpthread-1.dll libzstd.dll nss3.dll nssutil3.dll smime3.dll'.format(sys.version_info) # these appear superfluous, though unexpectedly so: # libcairo-2.dll libcairo-gobject-2.dll libfontconfig-1.dll libfreetype-6.dll libiconv-2.dll # libgettextlib-0-19-8-1.dll libgettextpo-0.dll libgettextsrc-0-19-8-1.dll libintl-8.dll libjasper-4.dll lib_gtk_dir = pathlib.Path(find_library('libgtk-3-0')).parent gdbus = pathlib.Path(find_library('gdbus.exe')) include_files = [(str(gdbus), str(pathlib.Path('lib', 'gi', 'gdbus.exe'))), (str(gdbus), 'gdbus.exe')] for lib in libs.split(): path = find_library(lib) path = pathlib.Path(path) if path is not None else path if path is not None and path.exists(): include_files.append((str(path), lib)) else: lib = pathlib.Path(lib) # Look in other directories? for path in lib_gtk_dir.glob(re.sub('-[0-9.]*$', '-*', lib.stem) + lib.suffix): include_files.append((str(path), path.name)) print('WARNING: Can not find library {}, including {} instead'.format(lib, path.name)) else: print('WARNING: Can not find library {}'.format(lib)) return include_files def check_cli_arg(val): """ Check whether an argument was passed, and clear it from sys.argv Returns (bool): whether the arguement was present """ if val in sys.argv[1:]: sys.argv.remove(val) return True return False def pympress_resources(): """ Return pympress resources. Only for frozen packages, as this is redundant with package_data. """ share = pathlib.Path('pympress', 'share') dirs = [share.joinpath('xml'), share.joinpath('pixmaps'), share.joinpath('css'), share.joinpath('defaults.conf')] translations = share.glob(str(pathlib.Path('*', 'LC_MESSAGES', 'pympress.mo'))) return [(str(f), str(f.relative_to('pympress'))) for f in dirs + list(translations)] if __name__ == '__main__': try: from babel.messages.frontend import compile_catalog except ImportError: compile_catalog = GettextBuildCatalog options = {'cmdclass': { 'build_py': BuildWithCatalogs, 'compile_catalog': compile_catalog, }} # subtle tweak: don’t put an install section in installed packages with open('README.md', encoding='utf-8') as f: readme = f.readlines() install_section = find_index_startstring(readme, '# Install') next_section = find_index_startstring(readme, '# ', install_section + 1) del readme[install_section:next_section] options['long_description'] = ''.join(readme) # Check whether to create a frozen distribution if check_cli_arg('--freeze'): print('Using cx_Freeze.setup():', file=sys.stderr) from cx_Freeze import setup, Executable setup(**{ **options, 'options': { 'build_exe': { 'includes': [], 'excludes': ['tkinter'], 'packages': ['codecs', 'gi', 'vlc', 'watchdog'], 'include_files': gtk_resources() + dlls() + pympress_resources(), 'silent': True }, 'bdist_msi': { 'add_to_path': True, 'all_users': False, 'summary_data': { 'comments': 'https://github.com/Cimbali/pympress/', 'keywords': 'pdf-viewer, beamer, presenter, slide, projector, pdf-reader, \ presentation, python, poppler, gtk, pygi, vlc', }, 'upgrade_code': '{5D156784-ED69-49FF-A972-CBAD312187F7}', 'install_icon': str(pathlib.Path('pympress', 'share', 'pixmaps', 'pympress.ico')), 'extensions': [{ 'extension': 'pdf', 'verb': 'open', 'executable': 'pympress-gui.exe', 'argument': '"%1"', 'mime': 'application/pdf', 'context': 'Open with p&ympress', }], } }, 'executables': [ Executable(str(pathlib.Path('pympress', '__main__.py')), target_name='pympress-gui.exe', base='Win32GUI', shortcut_dir='ProgramMenuFolder', shortcut_name='pympress', icon=str(pathlib.Path('pympress', 'share', 'pixmaps', 'pympress.ico'))), Executable(str(pathlib.Path('pympress', '__main__.py')), target_name='pympress.exe', base='Console', icon=str(pathlib.Path('pympress', 'share', 'pixmaps', 'pympress.ico'))), ] }) else: # Normal behaviour: use setuptools, load options from setup.cfg print('Using setuptools.setup():', file=sys.stderr) setuptools_version = tuple(int(n) for n in setuptools.__version__.split('.')[:2]) # older versions are missing out! if setuptools_version >= (30, 5): options['data_files'] = [ ('share/pixmaps/', ['pympress/share/pixmaps/pympress.png']), ('share/applications/', ['pympress/share/applications/pympress.desktop']), ] setuptools.setup(**options) ## # Local Variables: # mode: python # indent-tabs-mode: nil # py-indent-offset: 4 # fill-column: 80 # end: