From 5f44f7b187093ef290315b7f8766b540a31de35f Mon Sep 17 00:00:00 2001 From: Michele Calgaro Date: Sat, 13 Jun 2020 22:45:28 +0900 Subject: [PATCH] Initial code import from debian snapshot https://snapshot.debian.org/package/codeine/1.0.1-3.dfsg-3.1/ Signed-off-by: Michele Calgaro --- COPYING | 340 ++++++++++ ChangeLog | 52 ++ PKGBUILD | 17 + README | 48 ++ SConstruct | 168 +++++ VERSION | 1 + configure | 87 +++ misc/codeine.desktop | 9 + misc/codeine_part.desktop | 10 + misc/codeine_play_dvd.desktop | 39 ++ misc/codeinerc | 10 + misc/codeineui.rc | 33 + misc/cr128-app-codeine.png | Bin 0 -> 7426 bytes misc/cr16-app-codeine.png | Bin 0 -> 870 bytes misc/cr22-app-codeine.png | Bin 0 -> 1215 bytes misc/cr32-app-codeine.png | Bin 0 -> 1725 bytes misc/cr48-app-codeine.png | Bin 0 -> 2689 bytes misc/cr64-app-codeine.png | Bin 0 -> 3604 bytes po/codeine.pot | 484 ++++++++++++++ po/messages.sh | 60 ++ scons/codeine.py | 101 +++ scons/generic.py | 95 +++ scons/kde.py | 771 ++++++++++++++++++++++ scons/scons-mini.tar.bz2 | Bin 0 -> 58214 bytes src/FAQ | 13 + src/SConscript | 21 + src/TODO | 38 ++ src/app/SConscript | 59 ++ src/app/actions.cpp | 27 + src/app/actions.h | 26 + src/app/adjustSizeButton.cpp | 125 ++++ src/app/adjustSizeButton.h | 37 ++ src/app/analyzer.cpp | 131 ++++ src/app/analyzer.h | 75 +++ src/app/captureFrame.cpp | 296 +++++++++ src/app/config.h | 20 + src/app/extern.h | 28 + src/app/fht.cpp | 262 ++++++++ src/app/fht.h | 126 ++++ src/app/fullScreenAction.cpp | 96 +++ src/app/fullScreenAction.h | 27 + src/app/insertAspectRatioMenuItems.cpp | 24 + src/app/listView.cpp | 39 ++ src/app/main.cpp | 52 ++ src/app/mainWindow.cpp | 714 ++++++++++++++++++++ src/app/mainWindow.h | 75 +++ src/app/playDialog.cpp | 114 ++++ src/app/playDialog.h | 36 + src/app/playlistFile.cpp | 123 ++++ src/app/playlistFile.h | 36 + src/app/slider.cpp | 145 ++++ src/app/slider.h | 52 ++ src/app/stateChange.cpp | 195 ++++++ src/app/theStream.cpp | 144 ++++ src/app/theStream.h | 50 ++ src/app/videoSettings.cpp | 135 ++++ src/app/videoSettings.h | 26 + src/app/videoWindow.cpp | 380 +++++++++++ src/app/volumeAction.cpp | 114 ++++ src/app/volumeAction.h | 29 + src/app/xineConfig.cpp | 321 +++++++++ src/app/xineConfig.h | 69 ++ src/app/xineEngine.cpp | 876 +++++++++++++++++++++++++ src/app/xineEngine.h | 159 +++++ src/app/xineScope.c | 148 +++++ src/app/xineScope.h | 38 ++ src/codeine.h | 43 ++ src/debug.h | 263 ++++++++ src/mxcl.library.cpp | 19 + src/mxcl.library.h | 28 + src/part/SConscript | 12 + src/part/part.cpp | 83 +++ src/part/part.h | 38 ++ src/part/toolbar.cpp | 44 ++ src/part/toolbar.h | 18 + src/part/videoWindow.cpp | 192 ++++++ src/part/videoWindow.h | 94 +++ src/part/xineEngine.cpp | 345 ++++++++++ 78 files changed, 9005 insertions(+) create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 PKGBUILD create mode 100644 README create mode 100644 SConstruct create mode 100644 VERSION create mode 100755 configure create mode 100644 misc/codeine.desktop create mode 100644 misc/codeine_part.desktop create mode 100644 misc/codeine_play_dvd.desktop create mode 100644 misc/codeinerc create mode 100644 misc/codeineui.rc create mode 100644 misc/cr128-app-codeine.png create mode 100644 misc/cr16-app-codeine.png create mode 100644 misc/cr22-app-codeine.png create mode 100644 misc/cr32-app-codeine.png create mode 100644 misc/cr48-app-codeine.png create mode 100644 misc/cr64-app-codeine.png create mode 100644 po/codeine.pot create mode 100755 po/messages.sh create mode 100644 scons/codeine.py create mode 100644 scons/generic.py create mode 100644 scons/kde.py create mode 100644 scons/scons-mini.tar.bz2 create mode 100644 src/FAQ create mode 100644 src/SConscript create mode 100644 src/TODO create mode 100644 src/app/SConscript create mode 100644 src/app/actions.cpp create mode 100644 src/app/actions.h create mode 100644 src/app/adjustSizeButton.cpp create mode 100644 src/app/adjustSizeButton.h create mode 100644 src/app/analyzer.cpp create mode 100644 src/app/analyzer.h create mode 100644 src/app/captureFrame.cpp create mode 100644 src/app/config.h create mode 100644 src/app/extern.h create mode 100644 src/app/fht.cpp create mode 100644 src/app/fht.h create mode 100644 src/app/fullScreenAction.cpp create mode 100644 src/app/fullScreenAction.h create mode 100644 src/app/insertAspectRatioMenuItems.cpp create mode 100644 src/app/listView.cpp create mode 100644 src/app/main.cpp create mode 100644 src/app/mainWindow.cpp create mode 100644 src/app/mainWindow.h create mode 100644 src/app/playDialog.cpp create mode 100644 src/app/playDialog.h create mode 100644 src/app/playlistFile.cpp create mode 100644 src/app/playlistFile.h create mode 100644 src/app/slider.cpp create mode 100644 src/app/slider.h create mode 100644 src/app/stateChange.cpp create mode 100644 src/app/theStream.cpp create mode 100644 src/app/theStream.h create mode 100644 src/app/videoSettings.cpp create mode 100644 src/app/videoSettings.h create mode 100644 src/app/videoWindow.cpp create mode 100644 src/app/volumeAction.cpp create mode 100644 src/app/volumeAction.h create mode 100644 src/app/xineConfig.cpp create mode 100644 src/app/xineConfig.h create mode 100644 src/app/xineEngine.cpp create mode 100644 src/app/xineEngine.h create mode 100644 src/app/xineScope.c create mode 100644 src/app/xineScope.h create mode 100644 src/codeine.h create mode 100644 src/debug.h create mode 100644 src/mxcl.library.cpp create mode 100644 src/mxcl.library.h create mode 100644 src/part/SConscript create mode 100644 src/part/part.cpp create mode 100644 src/part/part.h create mode 100644 src/part/toolbar.cpp create mode 100644 src/part/toolbar.h create mode 100644 src/part/videoWindow.cpp create mode 100644 src/part/videoWindow.h create mode 100644 src/part/xineEngine.cpp diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..5b6e7c6 --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 Library 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 Library General +Public License instead of this License. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..22fcea1 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,52 @@ +1.0.1 + Mute button for KPart + Play DVD entry for KDE 3.5 + media:/ when DVD inserted + DVD-Menu-Toggle is no longer a KToggleAction because I can't detect when DVD menus change, but it still acts as a toggle button + Made record work for systems other than mine! (hard-coded path) + Made record shortcut CTRL-R so it doesn't conflict with the DVD-Root-Menu toggle + videoWindow doesn't judder when toolbar appears in fullscreen anymore + dvd-toolbar is gone, instead root menu button appears when dvd is playing + toolbar in fullscreen mode shows on mouse move + toolbar in fullscreen mode respects user-positioning + media kioslave support + double-clicking the video toggles fullscreen + don't show part in K-Menu + a volume toolbar button, - available from the configure-toolbar dialog, it's not very good yet + +1.0-rc2 + Seek fixes + Improved error messages + KPart crash on exit fix + +1.0-beta6 + Frame capture function + Aspect Ratio setting + Snap for videoSettings dialog sliders + Polish to all dialogs, menus + Hide cursor in fullscreen mode bug fixed + Many other fixes and lots of polish + +1.0-beta3 + Made Codeine single-window + Removed Pause KAction, instead toggling play pauses + Made it remember all details about how you like to view videos (eg. + contrast, brightness, size) + Shows toolbar when mouse is a screen-top in fullscreen mode + Bug fixes + +1.0-beta2 + Fixed fullscreen not covering Kicker + Added stop KAction + Added "You must install!" message after make does linking + Set busy cursor during unresponsive init period + Made the GUI detect lack of a ui file and show a useful message + If you quit during playback, the track volume fades out + Various feel/feedback fixes + Recent-files list in initial definately doesn't have duplicates anymore, sort order is also corrected + Routed out some nasty freeze bugs + Automatic frame format change handling + Stream recording + Many little bug-fixes and improvements + +1.0-beta1 + Initial release diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..34bc514 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,17 @@ +pkgname=codeine +pkgver=1.0.1 +pkgrel=1 +pkgdesc="A simple xine-based video player" +url="http://www.methylblue.com/codeine/" + +build() { + echo -e "\033[0;34m==>\033[0;0;1m Configure \033[0;0m" + cd "$startdir" + ./configure prefix=/opt/kde + + echo -e "\033[0;34m==>\033[0;0;1m Make \033[0;0m" + make || return 1 + + echo -e "\033[0;34m==>\033[0;0;1m Install \033[0;0m" + DESTDIR="$startdir/pkg" make install +} diff --git a/README b/README new file mode 100644 index 0000000..6f5ad6c --- /dev/null +++ b/README @@ -0,0 +1,48 @@ +INTRODUCTION + Codeine is a very simple xine-based media player. + + I make the following promises: + + * I will not add any substantial features after version 1.0.0 + * After then, improvements will only be in the realm of usability and bug + fixes + + You can rely on Codeine for now and until xine is obsolete to be a simple + no-frills video(/media) player. + + Visit #codeine on freenode.net! + + Max Howell + + +REQUIREMENTS + You will need at least: + + * Qt 3.3.0 (Qt 3.2.x may work, it is just not tested) + * KDElibs 3.3.0 + * xine-lib 1.0.0-rc4 + + You also need python installed in order to build Codeine. + + +INSTALLATION + I use scons + bksys as the build system. But you can still do the following: + + % ./configure && make && su -c "make install" + + Or if you have scons installed, simply: + + % scons && su -c "scons install" + + Note that scons is a little silly and this kind of thing doesn't work: + + % ./configure --prefix=/foo/bar --debug=full + + Instead do: + + % ./configure prefix=/foo/bar debug=full + + +TRANSLATIONS + I will make the po file available for translation at the 1.0-rc1 stage, if + you want to make a translation I thank you in advance :-) diff --git a/SConstruct b/SConstruct new file mode 100644 index 0000000..c08d4dd --- /dev/null +++ b/SConstruct @@ -0,0 +1,168 @@ +#!/usr/bin/python + +########################################### +## Common section, for loading the tools + +## Load the builders in config +env = Environment(TARGS=COMMAND_LINE_TARGETS, ARGS=ARGUMENTS, tools=['default', 'generic', 'kde', 'codeine'], toolpath=['./scons/']) + + +## the configuration should be done by now, quit +if 'configure' in COMMAND_LINE_TARGETS: + env.Exit(0) + + + +""" +Overview of the module system : + +Each module (kde.py, generic.py, sound.py..) tries to load a stored +configuration when run. If the stored configuration does not exist +or if 'configure' is given on the command line (scons configure), +the module launches the verifications and detectioins and stores +the results. Modules also call exit when the detection fail. + +For example, kde.py stores its config into kde.cache.py + +This has several advantages for both developers and users : + - Users do not have to run ./configure to compile + - The build is insensitive to environment changes + - The cache maintains the objects so the config can be changed often + - Each module adds its own help via env.Help("message") +""" + +## Use the variables available in the environment - unsafe, but moc, meinproc need it :-/ +import os +env.AppendUnique( ENV = os.environ ) +## If you do not want to copy the whole environment, you can use this instead (HOME is necessary for uic): +#env.AppendUnique( ENV = {'PATH' : os.environ['PATH'], 'HOME' : os.environ['HOME']} ) + +## The target make dist requires the python module shutil which is in 2.3 +env.EnsurePythonVersion(2, 3) + +## Bksys requires scons 0.96 +env.EnsureSConsVersion(0, 96) + +""" +Explanation of the 'env = Environment...' line : +* the command line arguments and targets are stored in env['TARGS'] and env['ARGS'] for use by the tools +* the part 'tools=['default', 'generic ..' detect and load the necessary functions for doing the things +* the part "toolpath=['./']" tells that the tools can be found in the current directory (generic.py, kde.py ..) +""" + +""" +To load more configuration modules one should only have to add the appropriate tool +ie: to detect alsa and add the proper cflags, ldflags .. + a file alsa.py file will be needed, and one should then use : + env = Environment(TARGS=COMMAND_LINE_TARGETS, ARGS=ARGUMENTS, tools=['default', 'generic', 'kde', 'alsa'], toolpath=['./']) + +You can also load environments that are targetted to different platforms +ie: if os.sys.platform = "darwin": + env = Environment(... + elsif os.sys.platform = "linux": + env = Environment(... + +""" + +## Setup the cache directory - this avoids recompiling the same files over and over again +## this is very handy when working with cvs +env.CacheDir('cache') +env.SConsignFile('scons/signatures') + +## If you need more libs and they rely on pkg-config +## ie: add support for GTK (source: the scons wiki on www.scons.org) +# env.ParseConfig('pkg-config --cflags --libs gtk+-2.0') + +""" +This tell scons that there are no rcs or sccs files - this trick +can speed up things a bit when having lots of #include +in the source code and for network file systems +""" +env.SourceCode(".", None) +dirs = [ '.', 'src', 'src/part', 'src/app' ] +for dir in dirs: + env.SourceCode(dir, None) + +## If we had only one program (named kvigor) to build, +## we could add before exporting the env (some kde +## helpers in kde.py need it) : +# env['APPNAME'] = 'kvigor' + +## Use this define if you are using the kde translation scheme (.po files) +env.Append( CPPFLAGS = ['-DQT_NO_TRANSLATION'] ) + +## Uncomment the following if you need threading support threading +#env.Append( CPPFLAGS = ['-DQT_THREAD_SUPPORT', '-D_REENTRANT'] ) +#if os.uname()[0] == "FreeBSD": +# env.Append(LINKFLAGS=["-pthread"]) + +## Important : export the environment so that SConscript files can the +## configuration and builders in it +Export("env") + + +def string_it(target, source, env): + print "Visit #codeine on irc.freenode.net!" + return 0 + +env.AddPostAction( "install", string_it ) + +env.SConscript( "src/SConscript", build_dir='build', duplicate=0 ) + + +if 'dist' in COMMAND_LINE_TARGETS: + + APPNAME = 'codeine' + VERSION = os.popen("cat VERSION").read().rstrip() + FOLDER = APPNAME+'-'+VERSION + ARCHIVE = FOLDER+'.tar.bz2' + + GREEN ="\033[92m" + NORMAL ="\033[0m" + + import shutil + import glob + + ## check if the temporary directory already exists + if os.path.isdir(FOLDER): + shutil.rmtree(FOLDER) + + ## create a temporary directory + startdir = os.getcwd() + # TODO copying the cache takes forever! delete it first + shutil.copytree(startdir, FOLDER) + + ## remove the unnecessary files + os.popen("find "+FOLDER+" -name \"{arch}\" | xargs rm -rf") + os.popen("find "+FOLDER+" -name \".arch-ids\" | xargs rm -rf") + os.popen("find "+FOLDER+" -name \".arch-inventory\" | xargs rm -f") + os.popen("find "+FOLDER+" -name \".scon*\" | xargs rm -rf") + os.popen("find "+FOLDER+" -name \"kdiss*-data\" | xargs rm -rf") + os.popen("find "+FOLDER+" -name \"*.pyc\" | xargs rm -f") + os.popen("find "+FOLDER+" -name \"*.cache.py\" | xargs rm -f") + os.popen("find "+FOLDER+" -name \"*.log\" | xargs rm -f") + os.popen("find "+FOLDER+" -name \"*.kdevelop.*\" | xargs rm -f") + os.popen("find "+FOLDER+" -name \"*~\" | xargs rm -f") + + os.popen("rm -rf "+FOLDER+"/autopackage") + os.popen("rm -rf "+FOLDER+"/build") + os.popen("rm -rf "+FOLDER+"/cache") + os.popen("rm -f " +FOLDER+"/codeine-*.tar.bz2") + os.popen("rm -f " +FOLDER+"/config.py*") + os.popen("rm -f " +FOLDER+"/src/configure.h") + os.popen("rm -f " +FOLDER+"/Doxyfile") + os.popen("rm -f " +FOLDER+"/Makefile") + os.popen("rm -rf "+FOLDER+"/packages") + os.popen("rm -rf "+FOLDER+"/screenshots") + os.popen("rm -f " +FOLDER+"/scons/signatures.dblite") + + ## make the tarball + print GREEN+"Writing archive "+ARCHIVE+NORMAL + os.popen("tar cjf "+ARCHIVE+" "+FOLDER) + + ## remove the temporary directory + if os.path.isdir(FOLDER): + shutil.rmtree(FOLDER) + + env.Default(None) + env.Exit(0) diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..7dea76e --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.1 diff --git a/configure b/configure new file mode 100755 index 0000000..83b4ab9 --- /dev/null +++ b/configure @@ -0,0 +1,87 @@ +#! /bin/sh +# TODO parse each passed argument and remove any "--" prefix + +BOLD="\033[1m" +RED="\033[91m" +GREEN="\033[92m" +YELLOW="\033[93m" +CYAN="\033[96m" +NORMAL="\033[0m" + +if command -v scons >/dev/null 2>&1; +then + SCONS=scons +else + if [ ! -e "scons/scons" ]; then + echo "" + echo -ne "Unpacking mini-scons..."$RED + + pushd scons >/dev/null 2>&1 + tar xjvf scons-mini.tar.bz2 > /dev/null 2>&1 + + if [[ "$?" == "0" ]]; then + echo -e $GREEN"done"$NORMAL + else + echo -e $RED"failed!"$NORMAL + exit 2 + fi + + popd > /dev/null + fi + + SCONS=scons/scons +fi + +if [[ "$1" == "--help" ]]; then + $SCONS -Q configure --help + exit +fi + +echo "" +echo "Configuring Codeine "`cat VERSION`"..." +echo "" + +#TODO remove all prefixed "--" + +$SCONS -Q configure $@ || exit 1 + +echo "" +echo -e "Your configure completed "$GREEN"successfully"$NORMAL", now type "$BOLD"make"$NORMAL +echo "" + +cat > Makefile << EOF +## Makefile automatically generated by unpack_local_scons.sh + +SCONS=$SCONS + +# scons : compile +# scons -c : clean +# scons install : install +# scons -c install : uninstall and clean + +# default target : use scons to build the programs +all: + \$(SCONS) -Q + +### There are several possibilities to help debugging : +# scons --debug=explain, scons --debug=tree .. +# +### To optimize the runtime, use +# scons --max-drift=1 --implicit-deps-unchanged +debug: + \$(SCONS) -Q --debug=tree + +clean: + \$(SCONS) -c + +install: + \$(SCONS) install + +uninstall: + \$(SCONS) -c install + +## this target creates a tarball of the project +dist: + \$(SCONS) dist +EOF + diff --git a/misc/codeine.desktop b/misc/codeine.desktop new file mode 100644 index 0000000..5def0f8 --- /dev/null +++ b/misc/codeine.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Codeine +Exec=codeine %u +Icon=codeine +Type=Application +Encoding=UTF-8 +MimeType=video/x-theora;video/x-ogm;video/x-ms-wmv;video/x-msvideo;video/x-ms-asf;video/x-matroska;video/mpeg;video/avi;video/quicktime;video/vnd.rn-realvideo;video/x-flic; +Categories=Qt;KDE;AudioVideo;Player +GenericName=Video Player diff --git a/misc/codeine_part.desktop b/misc/codeine_part.desktop new file mode 100644 index 0000000..728da15 --- /dev/null +++ b/misc/codeine_part.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Encoding=UTF-8 +Icon=codeine +MimeType=video/x-theora;video/x-ogm;video/x-ms-wmv;video/x-msvideo;video/x-ms-asf;video/x-matroska;video/mpeg;video/avi;video/quicktime;video/vnd.rn-realvideo;video/x-flic; +Name=Codeine +Comment=Embeddable Video Player +ServiceTypes=KParts/ReadOnlyPart +Type=Service +X-KDE-Library=libcodeine +InitialPreference=9 diff --git a/misc/codeine_play_dvd.desktop b/misc/codeine_play_dvd.desktop new file mode 100644 index 0000000..d1e9989 --- /dev/null +++ b/misc/codeine_play_dvd.desktop @@ -0,0 +1,39 @@ +[Desktop Entry] +ServiceTypes=media/dvdvideo +Actions=Play; +Encoding=UTF-8 +X-KDE-Priority=TopLevel + +[Desktop Action Play] +Name=Play DVD with Codeine +Name[bg]=Възпроизвеждане на DVD с Codeine +Name[bn]=ক্যাফিন দিয়ে ডিভিডি চালাও +Name[br]=Seniñ an DVD gant Codeine +Name[ca]=Reprodueix DVD amb Codeine +Name[cs]=Přehrát DVD v Codeine +Name[da]=Spil dvd med Codeine +Name[de]=DVD mit Codeine abspielen +Name[el]=Αναπαραγωγή DVD με το Codeine +Name[es]=Reproducir DVD con Codeine +Name[et]=Esita DVD Codeine'is +Name[fi]=Toista dvd-levy Codeinessa +Name[fr]=Lire le DVD avec Codeine +Name[ga]=Seinn DVD le Codeine +Name[he]=נגן תקליטור DVD עם Codeine +Name[it]=Riproduci DVD con Codeine +Name[ja]=CodeineでDVDを再生 +Name[nb]=Spill DVD med Codeine +Name[nl]=DVD met Codeine afspelen +Name[nn]=Spel DVD med Codeine +Name[pa]=ਕੈਫੀਨ ਨਾਲ DVD ਚਲਾਓ +Name[pl]=Odtwarzaj DVD w Codeine +Name[pt]=Ver o DVD com o Codeine +Name[pt_BR]=Reproduzir o DVD com o Codeine +Name[sr]=Пусти DVD Codeine-ом +Name[sr@Latn]=Pusti DVD Codeine-om +Name[sv]=Spela dvd med Codeine +Name[tr]=DVD'yi Codeine ile oynat +Name[xx]=xxPlay DVD with Codeinexx +Name[zh_CN]=用 Codeine 播放 DVD +Icon=codeine +Exec=codeine --play-dvd %u diff --git a/misc/codeinerc b/misc/codeinerc new file mode 100644 index 0000000..399baa9 --- /dev/null +++ b/misc/codeinerc @@ -0,0 +1,10 @@ +[KFileDialog Settings] +ShowPreviews=false + +[MainWindow Toolbar dvdToolBar] +Hidden=true +IconText=IconTextRight +Index=0 + +[MainWindow Toolbar mainToolBar] +Index=1 diff --git a/misc/codeineui.rc b/misc/codeineui.rc new file mode 100644 index 0000000..d6ef71f --- /dev/null +++ b/misc/codeineui.rc @@ -0,0 +1,33 @@ + + + + &Play + + + + + + + + &Settings + + + + + + + + + + + +Main Toolbar + + + + + + + + + diff --git a/misc/cr128-app-codeine.png b/misc/cr128-app-codeine.png new file mode 100644 index 0000000000000000000000000000000000000000..f01fd7edb0f521dc4765c5d9df9385ccb6df42b5 GIT binary patch literal 7426 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 literal 0 HcmV?d00001 diff --git a/misc/cr16-app-codeine.png b/misc/cr16-app-codeine.png new file mode 100644 index 0000000000000000000000000000000000000000..dab56c953789dc87bc82c8ae085b1318def04d06 GIT binary patch literal 870 zcmV-s1DX7ZP)rquzJ)WPR|5;B@ z?*ves10aA{7?8mK{|pR&|Ndq8^XE5+s{<4Xu(h>yW@Kd2Ha9mH3=0cmS+r;ggNlke z!;Kqv3@=@}qze#0j4;j0%1W}&o;~yCHq?WQCeEMPeeq-O;=Y>DJ?C7bJM1cKpT`8#Kfc+ znwuMd-eLdw^5yf_j~+cZ57e{|hz|qtS(wED0mPV@nf=Ag%z}IU`ZWxCdd3W!H?RM5 z>(-5rfB*iw{p;7ylRz~)fcP8`zX9TZAb&$K2z&qt06_r0{{-3D+1(%@Aua|62LST& z>;NGlApkNmF$fbA5+~i=;3Xj;9$oqQ`vT|Z=>jP!CIRE)~_pp^oM zSxQPW&feZZ_UF%^3@It84D9S|Ow7zIY>A0UoX?*><~Gl+{zGRVlt0A0rfbd53tKfeG2 z2L~rGTvQlXS=kwcg+&=OG&C5F961d1?xqs}0mSm_*RK^UEG!)+CPvcVzWoM9<$rJ* z0ERG711p&S=g(g-`|saBV2|_gurpk`bRHDtF98Awln!_pff4=d*FOfJVc>KC1HXU& zMq>Yhv$?p~8UFqI4@`KB%m4wz0!cvsfeQWt4fum%050(B*MFe8|6n#i0I|SQ;P2mm z!1&)k!Up{L15)=7%mxS`umK5vH^fItTPdHeP)!_Ax57`}b`1`ZrTfRB%t;nuC&K(GJ%1rR`>%*4dS#Z@9C wBoz1`6q>*w!LOYW9Qq%C;LDfKO#lG~05|m|f=!P2=l}o!07*qoM6N<$f&+tlu>b%7 literal 0 HcmV?d00001 diff --git a/misc/cr22-app-codeine.png b/misc/cr22-app-codeine.png new file mode 100644 index 0000000000000000000000000000000000000000..c8ed199c5d824bb2ff5ca859ba10224e2452ee93 GIT binary patch literal 1215 zcmV;w1VH+W_1nnO(#p!#*4pUn*B^2pKYrk@si|W)apDw1LPE3` zP~BvJ0Aj&s*q1L~xS5%mC0JS6jK##nojg3;%vDuYl`mYl#Ov(r%#f0j#BlK7QHH9j zGKSjPI);D$82$paDS{jf5I~GD!&q5a8Gir%4PvSQeXAoXDsE(LZDnm?VPW{>$x}g4 z5HvJ2GjMQlF=S*UGhDfHnZd}&^3RPMSKl5#e)#F%zkja)^_~ObV;~wJfItQd0kIl4 zH@B09hNg9BXowcj66tg2F0dvf#51_KI5W(eHJ@S1ls*O(6)lGE-+z2PefsFbw{PEG z1?oEw#2111E)YKh;?FRL0t67Fo}S+J!oouLQ>V@_%F4KnVvm+$p{K_ z85t>{uYNE*dGee=K!Be?TwIj()TvWXPo6wk01!Yxj(Fnb%UAv{UAp{#U0wZuprQYN z{`~d7wYBa4)Tz_{gTTFe5B`^wl>XnfYd4s`XU~EE#l@xnuV23b=FgkA@PA`t)BoSU z|NfsgZ8|7r!~+E2knC{)1fekYCB}<*AW!Qtlz~L_K@1Zv{KWw=4j^&t(k|^vsYA|r zjPdG;b~qQ{eE{biLaazB!&(c*7ASSJr7E=6jb|8h;(1<3DSvJbwD?W5O`iY)g$XFj z6|Y&dt|cHKfa&@3R}6pu{$t?c;sob1prv4%lamuD{uj(=VPOR(J9dU&zkUJnZ(vSk z1IlwTeEj&1!PnRO@uf=_;{XDPg^7vj7f{~?pvFH;OpI*6WCu0~l2I5KczJo@F%8Tt z@T?D#16c;bAOJK67(cuq2Z18*6F>m5aBy&dY-aff%r-#80HR?A!wkTk-4SdirhnjI zg63LAfB=GLX^^2npMrw{*b1N{P!;-P~U%eu>lZ3EYM`b%*DmU%E!kKRPdiHgF%_*|NlRrxMBsF4iG>rpu!#K z%J(Z*E`53V@Jn002ovPDHLkV1n8nGK~NL literal 0 HcmV?d00001 diff --git a/misc/cr32-app-codeine.png b/misc/cr32-app-codeine.png new file mode 100644 index 0000000000000000000000000000000000000000..6cbd81c46090149ead1f8b1ffa97d2a2a728a6e6 GIT binary patch literal 1725 zcmV;u215CXP)QBm zWA~%2t^NP~`}hB6Wo3aZtOH^$kV1d}VxfX%96+o9#HO5_T-JhuLZ)tRu9{w69-LZQ z+6)^uY+|@~?;gXCAKw|o#pN07>|7aU&YWra_wQd(pq_^S0Yt6?gn`(Mfq~IUSy{>0 z%F5Ev$jDgW+O?YutgI|ROY9j!LqixqfqVY^6^8x$_b^$iHHb0$jHcQiin7b3J3_Y#l%E0czC#j74`K^1X{M6VcD|B z3`|TM4FCQyFznc|o#EukQ-6N^`0?)4sZ$Su%I*R2MIinH(gP4cxE#O+#JWIi&BeuK zB`GPTr>Ll`k(3nA=-^<_z{e-Z(ACw!aO>7hhP`|DF{rB>GJOC36Bx|D87^G7!0_eE zmw$&3A9?@&{o6-CGadu+T_C;%r9u4HK>P`05kLU3AjP8uDDiM|a=Hr%iP*WiI!gxz z2J!=a$*^wSI-mvLfx*kpAR;2g@a@|l1}-jshHKZZFto3$)}mgPNKaLr6#r*wPg%7XCYQXy3PA zzka;~rrrloAKnMzJ5c&55WfZDU!LopfIQMv5CH-R<^U@vCzm~nii(_< zE?s6|W@cs(65{{I!ou<$=;QD2-+%fERQwkhHGjFd*#H0g_wPSYjPcK(zl`kcY>WZ| zyucLykKyy@Z{V~7)W;ww$PYA!3FHxmPoKVk>|o&M=LJSLJ1CJd?c29+IneATfB=Fe z!uQ(R8ei(`8#zG*$FE;M87wU=nG6i{xc~h5!@YIuc7`WUo-!yYDKdC^x`Wfv@#Ci% z&YnFBv`CP_&(E8ImzS5};lsxa+qdroTjuWW!l0la547wXxG?(o@goD!JO*oPONK{} z9s#3R%?0RyWdH#La{$oOtZc%gA?qtXO9_*ib{aFg&P<;e}E3# z#Bk=!aRwISKmZ!*AO%1Whyv&kf`!!5s~ZWn_5kiWMc^3(gK}lyBKi%q zoyG9@!=E2eLkOvqGD|kM zoW!ttHP9yv42=K*1PcLBDn81^#d(^Sm)o9&g$byM1za8=YE1~u#l-@|oG9WP9L(Uv zg|3#5j}=3VmzN7D4mKB*i{1kS5PDJk>(8Hmz~BYC=-*!&wLd^+gIxn6{s9CK6MDHo z?*!KX9oA!nA9HB7y-RfLO36 z5>P_?2h5(-cL2n52tGgnVT}Ti<+O4D$XsLq5I|TR0E+@zg#dCC00a;VrUFDFq)iB* zW&?l#!kGsC|Nl#qVgVAm2uA<}5RP&HmIi3!0AynU0tnp!jG&5xiHQ}6S*Y#+Q0TI; zvBRoku;Bm!#DeBJrWY@sGswxkX88K`3)OuOY;!Q&zWo~1kO8H{&mcDe1Q4t`hN*D` zHaaSSRjLFn5&+1apFe*x0x_t`z6gl70R#}b13*9qh^3(oM_Pd&(1z|ifB*vk8(OO9 T(=7^(00000NkvXXu0mjf6nz#x literal 0 HcmV?d00001 diff --git a/misc/cr48-app-codeine.png b/misc/cr48-app-codeine.png new file mode 100644 index 0000000000000000000000000000000000000000..f9f91806299f6b322d005b010b09fc4cbad6dea1 GIT binary patch literal 2689 zcmW+&dpy(o8~=Rw-E3@Q<~AhL4K>WA5@|!7gM?6=j$dXr>LjMj8MByr;)b zf0d;EI2bk69fXi)s{}IAb&o3m^<7(0c^_17!-QSI2>~$?2}xn`Y~ad@VY11dN5dl7 z{_HSTN?bR4I{+Fwo;zIwlV33vN+nI|rv{*Q$7e2pK*y1_aBjX-#0?_hXE5zRZo#(8 z=@eu;r7IeZAt&Rgc6OQ#d^v9b-LH!J(o*uiXsEEeW-=}~aJBN|52+NzCeJI*6u!QIQM0+ zZ)#~iMp)--Y8N%!a@x$oB9m9>o9`MB@W;$p$BYyGK^kvCouGsaH3;;ES)fC>0&Z9m zoW(b9rgn98d8n)B+I9Ez!0!9ibT8v+XB!+V$VZF<@mGU<+HvpYuKzwZ=@L|xQl?J#jXGoT1rnBhuucO5saOaJ(}eXy-u@hf;8qSEQ(y0|P^PdU~}f zDRPpYUMY94O{eIhMPE^Iu_kV>KBGYyJ~A>A&R{ZG$%+MSS(`Vmv(Pl$FQ^}Vft@Hk z(r;}a@*uT%bg20lPF0_fU@VfH!I^yXI-9<@qr`ofewdJf-8YZV;Xu?E7Ok>C4Bz$sYg^_-zR2u`wQ(OpHLNx-*wKFN&k1ASDRx+^-K>R z86Rg4+&Q{dI@BD%{U%CKte&upW<;eg%#U}!HI*bNR~zLeRaYyF9&yxaUD=ADi+m24 z4)x2;y+TyOkjF_Lh}1O|XU_&<%Z2jV8WyRxriQCI#+;g;kBE(rM{M9LB?*ruqN`V* zh}k_6u^SZAZ=IzV_84<(^X#N`zEvcjKb@Y4Im&2;MHTwd@`Ac&C!^Cv@OO;Ja}?!} za-S!hG9a%MoNLL20F<=~cDA>(8+tW3$kSJ{%6FV_l78y1Z!qYzq4{xQa^P><_235_nZA2> zW0va&QZRC5X=Qn->a0x_2mUArdx%NZ-B1~YvHl_Ykdd(VEv@0ArR$IJ#nSSCpDr~Q z6nHXKZOwK({+TQ-UW=A|SY^}^Dl(Jnf25bMb?qK8cVM~B#{4FvmZiQY5+*^%2H6)5 zguuLnMdJC1zG(xhaUa*y&&vyU@b42#b8v9beU!m%k!1AkTj-ssE7h3wX0UORCulQJ zsb%HmRM^14JeWpBQ7Iu!Q;@g>-h3-2(;qGTS;!+c$ zrxz9*7nkL{@cq+PT-w}c^ev@Aj=g&LHm~D~^^j=hy^U>1OS)djR_?6hwwL@~@GAnB z^<^-LpSRCr&^Pr)Beq>JUwvLEk)7&ETs|#9VrLLz1v?S;Jt&l)NWZm6`^Y@K{`ZJF**_1mbIez-vMtzpU2@(i@xd+(WcyQM zO$*Jo&7hNG-V@`u?}*b2)V9drBbRh4Bswz+dcTS8+MQW~gJJoj4d&Oo&NuyaC++zj z%eHOVVU7X!9Ojo#WMoWh(!!-KKi2I)WumtKjm(! zao|qQib9WcY{>6cEAI<@302X`>Si+R$Bp2NH7j#pmT2RtFqtd+gD5IvQwaAK3iP4rRMCP97q}JjuE=~GPOFyMg_}3mP#?8wCD&Eu z<{D01;)n-0*j|Z<`6oo?QcDO8`xY>P8r(IgGd%Pwn4BaTg0IEwaNFderh@5fS5}w) zrB(C?dhigoByGehkPDbh(wM5th4;|_f$=T;Oe4C;4y!jVt7wUvMK8~*rLD*yHt{8d z$PxG*OBA1RQ!4GlOJNLIUs0e$$AoEKMKhu$324>o@OI;e%^Fo114K^FItNE47}NCL zrt)B>yC`BgOs!@h??P$0S_C@f%uZ96(*$c3=}GrMR(Vn^yk zq&6rQ50J7VOF?Wc@(0Md6(t^_MIDRsQIV52@9n|gz`p?Yw(CBswOAn!dnvLL)Ak=9`fA3fB&5}S*c=STH2>#1fhb0lhG(V9lTz_lDV-d zz=3qB4OC!HGq~QH@eV-Xc(Wf*$BV*0Pivng;Y4CCe^b;N9+GIb(CMuDha lPX+47tQIPM)8hgdTz&qiGvL5wf{H@|&s{z{ue&ia{|`;iq%r^i literal 0 HcmV?d00001 diff --git a/misc/cr64-app-codeine.png b/misc/cr64-app-codeine.png new file mode 100644 index 0000000000000000000000000000000000000000..da5071350fc7615b34377b0ee2e4d4e8eaaf658b GIT binary patch literal 3604 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 literal 0 HcmV?d00001 diff --git a/po/codeine.pot b/po/codeine.pot new file mode 100644 index 0000000..47448a6 --- /dev/null +++ b/po/codeine.pot @@ -0,0 +1,484 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2005-08-01 17:30+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../src/app/videoSettings.cpp:91 +msgid "Video Settings" +msgstr "" + +#: ../src/app/playDialog.cpp:27 +msgid "Play Media" +msgstr "" + +#: ../src/app/playDialog.cpp:33 +msgid "What media would you like to play?" +msgstr "" + +#: ../src/app/playDialog.cpp:38 +msgid "Play File..." +msgstr "" + +#: ../src/app/playDialog.cpp:42 +msgid "Play VCD" +msgstr "" + +#: ../src/app/playDialog.cpp:46 +msgid "Play DVD" +msgstr "" + +#: ../src/app/playDialog.cpp:73 +msgid "Recently Played Media" +msgstr "" + +#: ../src/app/xineConfig.cpp:60 +msgid "Configure xine" +msgstr "" + +#: ../src/app/xineConfig.cpp:84 +msgid "" +"xine's defaults are usually sensible and should not require modification. " +"However, full configurability is provided for your pleasure ;-)." +msgstr "" + +#: ../src/app/adjustSizeButton.cpp:31 +msgid "Preferred Scale" +msgstr "" + +#: ../src/app/adjustSizeButton.cpp:35 +msgid "Scale 100%" +msgstr "" + +#: ../src/app/adjustSizeButton.cpp:41 +msgid "Adjust video scale?" +msgstr "" + +#: ../src/app/insertAspectRatioMenuItems.cpp:15 +msgid "Determine &Automatically" +msgstr "" + +#: ../src/app/insertAspectRatioMenuItems.cpp:17 +msgid "&Square (1:1)" +msgstr "" + +#: ../src/app/insertAspectRatioMenuItems.cpp:18 +msgid "&4:3" +msgstr "" + +#: ../src/app/insertAspectRatioMenuItems.cpp:19 +msgid "Ana&morphic (16:9)" +msgstr "" + +#: ../src/app/insertAspectRatioMenuItems.cpp:20 +msgid "&DVB (2.11:1)" +msgstr "" + +#: ../src/app/main.cpp:14 +msgid "A video player that has a usability focus" +msgstr "" + +#: ../src/app/main.cpp:15 +msgid "Copyright 2005, Max Howell" +msgstr "" + +#: ../src/app/main.cpp:19 +msgid "Play 'URL'" +msgstr "" + +#: ../src/app/main.cpp:28 +msgid "Handbook" +msgstr "" + +#: ../src/app/main.cpp:29 +msgid "Great reference code" +msgstr "" + +#: ../src/app/main.cpp:30 +msgid "The current Codeine icon" +msgstr "" + +#: ../src/app/main.cpp:31 +msgid "The video for \"Call on Me\" encouraged plenty of debugging! ;)" +msgstr "" + +#: ../src/app/playlistFile.cpp:32 +msgid "The file is not a playlist" +msgstr "" + +#: ../src/app/playlistFile.cpp:39 +msgid "Codeine could not download the remote playlist: %1" +msgstr "" + +#: ../src/app/playlistFile.cpp:54 +msgid "" +"The playlist, '%1', could not be interpreted. Perhaps it is empty?" +msgstr "" + +#: ../src/app/playlistFile.cpp:58 +msgid "Codeine could not open the file: %1" +msgstr "" + +#: ../src/app/stateChange.cpp:83 +msgid "&Pause" +msgstr "" + +#: ../src/app/stateChange.cpp:83 +msgid "&Play" +msgstr "" + +#: ../src/app/stateChange.cpp:147 +msgid "No media loaded" +msgstr "" + +#: ../src/app/stateChange.cpp:150 +msgid "Paused" +msgstr "" + +#: ../src/app/actions.cpp:13 ../src/part/part.cpp:38 +msgid "Play" +msgstr "" + +#: ../src/app/xineEngine.cpp:127 ../src/part/xineEngine.cpp:50 +msgid "xine was unable to initialize any video-drivers." +msgstr "" + +#: ../src/app/xineEngine.cpp:129 ../src/part/xineEngine.cpp:48 +msgid "xine was unable to initialize any audio-drivers." +msgstr "" + +#: ../src/app/xineEngine.cpp:231 +msgid "Loading media: %1" +msgstr "" + +#: ../src/app/xineEngine.cpp:333 +msgid "Recording to: %1" +msgstr "" + +#: ../src/app/xineEngine.cpp:364 +msgid "Playback paused" +msgstr "" + +#: ../src/app/xineEngine.cpp:369 +msgid "Playback resumed" +msgstr "" + +#: ../src/app/xineEngine.cpp:382 +msgid "There is no input plugin that can read: %1." +msgstr "" + +#: ../src/app/xineEngine.cpp:385 +msgid "There is no demux plugin available for %1." +msgstr "" + +#: ../src/app/xineEngine.cpp:388 +msgid "Demuxing failed for %1." +msgstr "" + +#: ../src/app/xineEngine.cpp:393 +msgid "Internal error while attempting to play %1." +msgstr "" + +#: ../src/app/xineEngine.cpp:433 +msgid "xine cannot currently seek in flac media" +msgstr "" + +#: ../src/app/xineEngine.cpp:585 ../src/app/xineEngine.cpp:593 +msgid "Channel %1" +msgstr "" + +#: ../src/app/xineEngine.cpp:692 ../src/part/xineEngine.cpp:282 +msgid "The source is encrypted and can not be decrypted." +msgstr "" + +#: ../src/app/xineEngine.cpp:694 ../src/part/xineEngine.cpp:284 +msgid "The host is unknown for the URL: %1" +msgstr "" + +#: ../src/app/xineEngine.cpp:696 ../src/part/xineEngine.cpp:286 +msgid "The device name you specified seems invalid." +msgstr "" + +#: ../src/app/xineEngine.cpp:698 ../src/part/xineEngine.cpp:288 +msgid "The network appears unreachable." +msgstr "" + +#: ../src/app/xineEngine.cpp:700 ../src/part/xineEngine.cpp:290 +msgid "Audio output unavailable; the device is busy." +msgstr "" + +#: ../src/app/xineEngine.cpp:702 ../src/part/xineEngine.cpp:292 +msgid "The connection was refused for the URL: %1" +msgstr "" + +#: ../src/app/xineEngine.cpp:704 ../src/part/xineEngine.cpp:294 +msgid "xine could not find the URL: %1" +msgstr "" + +#: ../src/app/xineEngine.cpp:706 ../src/part/xineEngine.cpp:296 +msgid "Access was denied for the URL: %1" +msgstr "" + +#: ../src/app/xineEngine.cpp:708 ../src/part/xineEngine.cpp:298 +msgid "The source cannot be read for the URL: %1" +msgstr "" + +#: ../src/app/xineEngine.cpp:710 ../src/part/xineEngine.cpp:300 +msgid "A problem occurred while loading a library or decoder." +msgstr "" + +#: ../src/app/xineEngine.cpp:737 ../src/part/xineEngine.cpp:327 +msgid "Sorry, no additional information is available." +msgstr "" + +#: ../src/app/captureFrame.cpp:82 +msgid "Capture - %1" +msgstr "" + +#: ../src/app/captureFrame.cpp:98 +msgid "" +"*.png|PNG Format\n" +"*.jpeg|JPEG Format" +msgstr "" + +#: ../src/app/captureFrame.cpp:100 +msgid "Save Frame" +msgstr "" + +#: ../src/app/captureFrame.cpp:111 +msgid "%1 saved successfully" +msgstr "" + +#: ../src/app/captureFrame.cpp:113 +msgid "Sorry, could not save %1" +msgstr "" + +#: ../src/app/fullScreenAction.cpp:31 +msgid "Exit F&ull Screen Mode" +msgstr "" + +#: ../src/app/fullScreenAction.cpp:37 +msgid "F&ull Screen Mode" +msgstr "" + +#: ../src/app/mainWindow.cpp:94 +msgid "&Subtitles" +msgstr "" + +#: ../src/app/mainWindow.cpp:95 +msgid "A&udio Channels" +msgstr "" + +#: ../src/app/mainWindow.cpp:96 +msgid "Aspect &Ratio" +msgstr "" + +#: ../src/app/mainWindow.cpp:106 +msgid "" +msgstr "" + +#: ../src/app/mainWindow.cpp:106 +msgid " could not load its interface, this probably means that " +msgstr "" + +#: ../src/app/mainWindow.cpp:106 +msgid "" +" is not installed to the correct prefix. If you installed from packages " +"please contact the packager, if you installed from source please try running " +"the configure script again like this:
 % ./configure --"
+"prefix=`kde-config --prefix`
" +msgstr "" + +#: ../src/app/mainWindow.cpp:139 +msgid "xine could not be successfully initialised. " +msgstr "" + +#: ../src/app/mainWindow.cpp:139 +msgid "" +" will now exit. You can try to identify what is wrong with your xine " +"installation using the xine-check command at a command-prompt." +msgstr "" + +#: ../src/app/mainWindow.cpp:208 +msgid "Play &Media..." +msgstr "" + +#: ../src/app/mainWindow.cpp:214 +msgid "Record" +msgstr "" + +#: ../src/app/mainWindow.cpp:216 +msgid "Reset Video Scale" +msgstr "" + +#: ../src/app/mainWindow.cpp:217 ../src/app/mainWindow.cpp:423 +msgid "Media Information" +msgstr "" + +#: ../src/app/mainWindow.cpp:218 +msgid "Menu Toggle" +msgstr "" + +#: ../src/app/mainWindow.cpp:219 +msgid "&Capture Frame" +msgstr "" + +#: ../src/app/mainWindow.cpp:221 +msgid "Video Settings..." +msgstr "" + +#: ../src/app/mainWindow.cpp:222 +msgid "Configure xine..." +msgstr "" + +#: ../src/app/mainWindow.cpp:224 +msgid "Position Slider" +msgstr "" + +#: ../src/app/mainWindow.cpp:317 +msgid "Codeine was asked to open an empty URL; it cannot." +msgstr "" + +#: ../src/app/mainWindow.cpp:366 +msgid "Supported Media Formats" +msgstr "" + +#: ../src/app/mainWindow.cpp:366 +msgid "All Files" +msgstr "" + +#: ../src/app/mainWindow.cpp:367 +msgid "Select A File To Play" +msgstr "" + +#: ../src/app/mainWindow.cpp:438 +msgid "&Determine Automatically" +msgstr "" + +#: ../src/app/mainWindow.cpp:450 +msgid "&Off" +msgstr "" + +#: ../src/app/mainWindow.cpp:492 +msgid "Sorry, no media was found in the drop" +msgstr "" + +#: ../src/app/theStream.cpp:107 +msgid "Metadata" +msgstr "" + +#: ../src/app/theStream.cpp:109 +msgid "Title" +msgstr "" + +#: ../src/app/theStream.cpp:110 +msgid "Comment" +msgstr "" + +#: ../src/app/theStream.cpp:111 +msgid "Artist" +msgstr "" + +#: ../src/app/theStream.cpp:112 +msgid "Genre" +msgstr "" + +#: ../src/app/theStream.cpp:113 +msgid "Album" +msgstr "" + +#: ../src/app/theStream.cpp:114 +msgid "Year" +msgstr "" + +#: ../src/app/theStream.cpp:116 +msgid "Audio Properties" +msgstr "" + +#: ../src/app/theStream.cpp:118 +msgid "Bitrate" +msgstr "" + +#: ../src/app/theStream.cpp:118 +msgid "%1 bps" +msgstr "" + +#: ../src/app/theStream.cpp:119 +msgid "Sample-rate" +msgstr "" + +#: ../src/app/theStream.cpp:119 +msgid "%1 Hz" +msgstr "" + +#: ../src/app/theStream.cpp:121 +msgid "Technical Information" +msgstr "" + +#: ../src/app/theStream.cpp:123 +msgid "Video Codec" +msgstr "" + +#: ../src/app/theStream.cpp:124 +msgid "Audio Codec" +msgstr "" + +#: ../src/app/theStream.cpp:125 +msgid "System Layer" +msgstr "" + +#: ../src/app/theStream.cpp:126 +msgid "Input Plugin" +msgstr "" + +#: ../src/app/theStream.cpp:127 +msgid "CDINDEX_DISCID" +msgstr "" + +#: ../src/app/videoWindow.cpp:140 +msgid "Pause" +msgstr "" + +#: ../src/part/xineEngine.cpp:159 +msgid "The Codeine video player could not find an input plugin for '%1'." +msgstr "" + +#: ../src/part/xineEngine.cpp:162 +msgid "The Codeine video player could not find a demux plugin for '%1'." +msgstr "" + +#: ../src/part/xineEngine.cpp:165 +msgid "" +"The Codeine video player failed to demux '%1'; please check your xine " +"installation." +msgstr "" + +#: ../src/part/xineEngine.cpp:170 +msgid "" +"The Codeine video player reports an internal error; please check your xine " +"installation." +msgstr "" + +#: ../src/_translatorinfo.cpp:1 +msgid "" +"_: NAME OF TRANSLATORS\n" +"Your names" +msgstr "" + +#: ../src/_translatorinfo.cpp:3 +msgid "" +"_: EMAIL OF TRANSLATORS\n" +"Your emails" +msgstr "" diff --git a/po/messages.sh b/po/messages.sh new file mode 100755 index 0000000..f3103f9 --- /dev/null +++ b/po/messages.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# Inspired by Makefile.common from coolo +# this script is used to update the .po files + +# To update the translations, you will need a specific gettext +# patched for kde and a lot of patience, tenacity, luck, time .. + + +# I guess one should only update the .po files when all .cpp files +# are generated (after a make or scons) + +# If you have a better way to do this, do not keep that info +# for yourself and help me to improve this script, thanks +# (tnagyemail-mail tat yahoo d0tt fr) + +SRCDIR=../src # srcdir is the directory containing the source code +TIPSDIR=$SRCDIR # tipsdir is the directory containing the tips + +KDEDIR=`kde-config --prefix` +EXTRACTRC=extractrc +KDEPOT=`kde-config --prefix`/include/kde.pot +XGETTEXT="xgettext -C -ki18n -ktr2i18n -kI18N_NOOP -ktranslate -kaliasLocale -x $KDEPOT " + +## check that kde.pot is available +if ! test -e $KDEPOT; then + echo "$KDEPOT does not exist, there is something wrong with your installation!" + XGETTEXT="xgettext -C -ki18n -ktr2i18n -kI18N_NOOP -ktranslate -kaliasLocale " +fi + +> rc.cpp + +## extract the strings +echo "extracting the strings" + +# process the .ui and .rc files +$EXTRACTRC `find $SRCDIR -iname *.rc` >> rc.cpp +$EXTRACTRC `find $SRCDIR -iname *.ui` >> rc.cpp +echo -e 'i18n("_: NAME OF TRANSLATORS\\n"\n"Your names")\ni18n("_: EMAIL OF TRANSLATORS\\n"\n"Your emails")' > $SRCDIR/_translatorinfo.cpp + +# process the tips - $SRCDIR is supposed to be where the tips are living +pushd $TIPSDIR; preparetips >tips.cpp; popd + +$XGETTEXT `find $SRCDIR -name "*.cpp"` -o codeine.pot + +# remove the intermediate files +rm -f $TIPSDIR/tips.cpp +rm -f rc.cpp +rm -f $SRCDIR/_translatorinfo.cpp + +## now merge the .po files .. +echo "merging the .po files" + +for i in `ls *.po`; do + msgmerge $i kdissert.pot -o $i || exit 1 +done + +## finished +echo "Done" + diff --git a/scons/codeine.py b/scons/codeine.py new file mode 100644 index 0000000..742cc78 --- /dev/null +++ b/scons/codeine.py @@ -0,0 +1,101 @@ +## Max Howell, 2005 + +BOLD ="\033[1m" +RED ="\033[91m" +GREEN ="\033[92m" +YELLOW ="\033[93m" +CYAN ="\033[96m" +NORMAL ="\033[0m" + +import os + +def exists( env ): + return true + +def generate( env ): + + if 'configure' in env['TARGS']: + xine_lib_test_source_file = """ + #include + #include + + int main( int argc, char **argv ) + { + if( XINE_MAJOR_VERSION < 1 ) + return 1; + + const QString version( XINE_VERSION ); + + // eg. VERSION 1.0 + if( version[1] == '.' ) + return 0; + + if( version == "1-cvs" ) + return 0; + + if( version.startsWith( "1-rc" ) && QString(version[4]).toInt() > 3 ) + return 0; + + return 2; //too old + }""" + + def CheckKdeLibs( context ): + # ideally should be able to tell bksys what version we need + context.Message( 'Checking for KDElibs 3.3...' ) + kde_version = os.popen("kde-config --version|grep KDE").read().strip().split()[1] + result = int( kde_version[0] ) == 3 and int( kde_version[2] ) >= 3 + context.Result( result ) + return result + + def CheckXineLib( context ): + context.Message('Checking for xine-lib 1.0...') + result = context.TryLink(xine_lib_test_source_file, '.cpp') + context.Result(result) + return result + + + # prolly best to use a fresh env + # this seems to import the user's CXXFLAGS, etc., which may break + confenv = env.Copy() + configure = confenv.Configure(custom_tests = {'CheckXineLib' : CheckXineLib, 'CheckKdeLibs' : CheckKdeLibs}, log_file='configure.log') + confenv.AppendUnique(LIBS = 'qt-mt') + confenv.AppendUnique(LINKFLAGS = '-L/usr/X11R6/lib') + + if not configure.CheckKdeLibs(): + print # 1 2 3 4 5 6 7 8' + print 'Configure could not detect KDElibs 3.3, which is required for Codeine to ' + print 'compile.' + print + confenv.Exit( 1 ) + + if not configure.CheckLibWithHeader( 'xine', 'xine.h', 'c++' ): + print # 1 2 3 4 5 6 7 8' + print 'Configure could not find either the xine library or header on your system. You ' + print 'should ammend the relevant paths. If you know which ones please email me so I ' + print 'can update this message!' + print + confenv.Exit( 2 ) + + if not configure.CheckXineLib(): + print # 1 2 3 4 5 6 7 8' + print 'Your xine-lib is either too old, or can not be linked against. Sorry for not ' + print 'being more specific..' + print + confenv.Exit( 3 ) + + if not configure.CheckLibWithHeader( 'Xtst', 'X11/extensions/XTest.h', 'c' ): + print # 1 2 3 4 5 6 7 8' + print 'libxtst was not found, this means the screensaver cannot be disabled during ' + print 'playback. YOU CAN STILL BUILD CODEINE! :)' + print + + file = open ( 'src/configure.h', 'w' ) + file.write( "#define NO_XTEST_EXTENSION\n" ) + file.close() + else: + # FIXME - thus only one thing can be in configure.h - lol + file = open ( 'src/configure.h', 'w' ) + file.write( "" ) + file.close() + + env = configure.Finish() diff --git a/scons/generic.py b/scons/generic.py new file mode 100644 index 0000000..3249df7 --- /dev/null +++ b/scons/generic.py @@ -0,0 +1,95 @@ +## Thomas Nagy, 2005 + +""" +Detect and store the most common options +* kdecxxflags : debug=1 (-g) or debug=full (-g3, slower) + else use the user CXXFLAGS if any, - or -O2 by default +* prefix : the installation path +* extraincludes : a list of paths separated by ':' +ie: scons configure debug=full prefix=/usr/local extraincludes=/tmp/include:/usr/local +""" + +BOLD ="\033[1m" +RED ="\033[91m" +GREEN ="\033[92m" +YELLOW ="\033[93m" +CYAN ="\033[96m" +NORMAL ="\033[0m" + +import os + +def exists(env): + return true + +def generate(env): + env.Help(""" +"""+BOLD+ +"""*** Generic options *** +-----------------------"""+NORMAL+""" +"""+BOLD+"""* debug """+NORMAL+""": debug=1 (-g) or debug=full (-g3, slower) else use environment CXXFLAGS, or -O2 by default +"""+BOLD+"""* prefix """+NORMAL+""": the installation path +"""+BOLD+"""* extraincludes """+NORMAL+""": a list of paths separated by ':' +ie: """+BOLD+"""scons configure debug=full prefix=/usr/local extraincludes=/tmp/include:/usr/local +"""+NORMAL) + + # load the options + from SCons.Options import Options, PathOption + opts = Options('generic.cache.py') + opts.AddOptions( + ( 'KDECXXFLAGS', 'debug level for the project : full or just anything' ), + ( 'PREFIX', 'prefix for installation' ), + ( 'EXTRAINCLUDES', 'extra include paths for the project' ), + ) + opts.Update(env) + + # use this to avoid an error message 'how to make target configure ?' + env.Alias('configure', None) + + # configure the environment if needed + if 'configure' in env['TARGS'] or not env.has_key('KDECXXFLAGS'): + # need debugging ? + if env.has_key('KDECXXFLAGS'): + env.__delitem__('KDECXXFLAGS') + if env['ARGS'].get('debug', None): + debuglevel = env['ARGS'].get('debug', None) + print CYAN+'** Enabling debug for the project **' + NORMAL + if (debuglevel == "full"): + env['KDECXXFLAGS'] = ['-DDEBUG', '-ggdb', '-pipe', '-Wall'] + else: + env['KDECXXFLAGS'] = ['-DDEBUG', '-g'] + else: + if os.environ.has_key('CXXFLAGS'): + # user-defined flags (gentooers will be delighted) + import SCons.Util + env['KDECXXFLAGS'] = SCons.Util.CLVar( os.environ['CXXFLAGS'] ) + env.Append( KDECXXFLAGS = ['-DNDEBUG', '-DNO_DEBUG'] ) + else: + env.Append(KDECXXFLAGS = ['-O2', '-DNDEBUG', '-DNO_DEBUG']) + + # user-specified prefix + if env['ARGS'].get('prefix', None): + env['PREFIX'] = env['ARGS'].get('prefix', None) + print CYAN+'** set the installation prefix for the project : ' + env['PREFIX'] +' **'+ NORMAL + elif env.has_key('PREFIX'): + env.__delitem__('PREFIX') + + # user-specified include paths + env['EXTRAINCLUDES'] = env['ARGS'].get('extraincludes', None) + if env['ARGS'].get('extraincludes', None): + print CYAN+'** set extra include paths for the project : ' + env['EXTRAINCLUDES'] +' **'+ NORMAL + elif env.has_key('EXTRAINCLUDES'): + env.__delitem__('EXTRAINCLUDES') + + # and finally save the options in a cache + opts.Save('generic.cache.py', env) + + if env.has_key('KDECXXFLAGS'): + # load the flags + env.AppendUnique( CPPFLAGS = env['KDECXXFLAGS'] ) + + if env.has_key('EXTRAINCLUDES'): + incpaths = [] + for dir in str(env['EXTRAINCLUDES']).split(':'): + incpaths.append( dir ) + env.Append(CPPPATH = incpaths) + diff --git a/scons/kde.py b/scons/kde.py new file mode 100644 index 0000000..8fa492b --- /dev/null +++ b/scons/kde.py @@ -0,0 +1,771 @@ +# Copyright (c) 2001, 2002, 2003, 2004 The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +# Shamelessly stolen from qt.py and (heavily) modified into kde.py :) +# Thomas Nagy, 2004, 2005 + +""" +Here follow the basic rules for building kde programs +The detection is done in detect_kde when needed +We wan to use the cached variables as much as possible + +The variables used when configuring are : +* prefix : base install path, eg: /usr/local +* execprefix : install path for binaries, eg: /usr/bin +* datadir : install path for the data, eg: /usr/local/share +* libdir : install path for the libs, eg: /usr/lib + +* libsuffix : for those who need /usr/lib64 and the like .. + +* kdeincludes: path to the kde includes (/usr/include/kde on debian, ...) +* qtincludes : same punishment, for qt includes (/usr/include/qt on debian, ...) + +* kdelibs : path to the kde libs, for linking the programs +* qtlibs : same punishment, for qt libraries + +eg: scons configure libdir=/usr/local/lib qtincludes=/usr/include/qt +""" + +BOLD ="\033[1m" +RED ="\033[91m" +GREEN ="\033[92m" +YELLOW ="\033[93m" +CYAN ="\033[96m" +NORMAL ="\033[0m" + +def exists(env): + return True + +def detect_kde(env): + """ Detect the qt and kde environment using kde-config mostly """ + import os, sys, re + + prefix = env['ARGS'].get('prefix', None) + execprefix = env['ARGS'].get('execprefix', None) + datadir = env['ARGS'].get('datadir', None) + libdir = env['ARGS'].get('libdir', None) + libsuffix = env['ARGS'].get('libsuffix', '') + kdeincludes = env['ARGS'].get('kdeincludes', None) + kdelibs = env['ARGS'].get('kdelibs', None) + qtincludes = env['ARGS'].get('qtincludes', None) + qtlibs = env['ARGS'].get('qtlibs', None) + + if libdir: + libdir = libdir+libsuffix + + ## Detect the kde libraries + print "Checking for kde-config : ", + kde_config = os.popen("which kde-config 2>/dev/null").read().strip() + if len(kde_config): + print GREEN + "kde-config was found" + NORMAL + else: + print RED + "kde-config was NOT found in your PATH"+ NORMAL + print "Make sure kde is installed properly" + print "(missing package kdebase-devel?)" + # TODO : prompt the user for the path of kde-config ? + sys.exit(1) + env['KDEDIR'] = os.popen('kde-config -prefix').read().strip() + + print "Checking for kde version : ", + kde_version = os.popen("kde-config --version|grep KDE").read().strip().split()[1] + if int(kde_version[0]) != 3 or int(kde_version[2]) < 2: + print RED + kde_version + print RED + "Your kde version can be too old" + NORMAL + print RED + "Please make sure kde is at least 3.2" + NORMAL + else: + print GREEN + kde_version + NORMAL + + ## Detect the qt library + print "Checking for the qt library : ", + qtdir = os.getenv("QTDIR") + if qtdir: + print GREEN + "qt is in " + qtdir + NORMAL + else: + m = re.search('(.*)/lib/libqt.*', os.popen('ldd `kde-config --expandvars --install lib`' + '/libkdeui.so.4 | grep libqt').read().strip().split()[2]) + if m: + qtdir = m.group(1) + print YELLOW + "qt was found as " + m.group(1) + NORMAL + else: + print RED + "qt was not found" + NORMAL + print RED + "Please set QTDIR first (/usr/lib/qt3?)" + NORMAL + sys.exit(1) + env['QTDIR'] = qtdir.strip() + + ## Find the necessary programs uic and moc + print "Checking for uic : ", + uic = qtdir + "/bin/uic" + if os.path.isfile(uic): + print GREEN + "uic was found as " + uic + NORMAL + else: + uic = os.popen("which uic 2>/dev/null").read().strip() + if len(uic): + print YELLOW + "uic was found as " + uic + NORMAL + else: + uic = os.popen("which uic 2>/dev/null").read().strip() + if len(uic): + print YELLOW + "uic was found as " + uic + NORMAL + else: + print RED + "uic was not found - set QTDIR put it in your PATH ?" + NORMAL + sys.exit(1) + env['QT_UIC'] = uic + + print "Checking for moc : ", + moc = qtdir + "/bin/moc" + if os.path.isfile(moc): + print GREEN + "moc was found as " + moc + NORMAL + else: + moc = os.popen("which moc 2>/dev/null").read().strip() + if len(moc): + print YELLOW + "moc was found as " + moc + NORMAL + elif os.path.isfile("/usr/share/qt3/bin/moc"): + moc = "/usr/share/qt3/bin/moc" + print YELLOW + "moc was found as " + moc + NORMAL + else: + print RED + "moc was not found - set QTDIR or put it in your PATH ?" + NORMAL + sys.exit(1) + env['QT_MOC'] = moc + + ## check for the qt and kde includes + print "Checking for the qt includes : ", + if qtincludes and os.path.isfile(qtincludes + "/qlayout.h"): + # The user told where to look for and it looks valid + print GREEN + "ok " + qtincludes + NORMAL + else: + if os.path.isfile(qtdir + "/include/qlayout.h"): + # Automatic detection + print GREEN + "ok " + qtdir + "/include/ " + NORMAL + qtincludes = qtdir + "/include/" + elif os.path.isfile("/usr/include/qt3/qlayout.h"): + # Debian probably + print YELLOW + "the qt headers were found in /usr/include/qt3/ " + NORMAL + qtincludes = "/usr/include/qt3" + else: + print RED + "the qt headers were not found" + NORMAL + sys.exit(1) + + print "Checking for the kde includes : ", + kdeprefix = os.popen("kde-config --prefix").read().strip() + if not kdeincludes: + kdeincludes = kdeprefix+"/include/" + if os.path.isfile(kdeincludes + "/klineedit.h"): + print GREEN + "ok " + kdeincludes + NORMAL + else: + if os.path.isfile(kdeprefix+"/include/kde/klineedit.h"): + # Debian, Fedora probably + print YELLOW + "the kde headers were found in " + kdeprefix + "/include/kde/" + NORMAL + kdeincludes = kdeprefix + "/include/kde/" + else: + print RED + "The kde includes were NOT found" + NORMAL + sys.exit(1) + + if prefix: + ## use the user-specified prefix + if not execprefix: + execprefix = prefix + if not datadir: + datadir = prefix + "/share" + if not libdir: + libdir = execprefix + "/lib"+libsuffix + + subst_vars = lambda x: x.replace('${exec_prefix}', execprefix).replace('${datadir}', + datadir).replace('${libdir}', libdir) + debian_fix = lambda x: x.replace('/usr/share', '${datadir}') + env['KDEBIN'] = subst_vars(os.popen('kde-config --install exe').read().strip()) + env['KDEAPPS'] = subst_vars(os.popen('kde-config --install apps').read().strip()) + env['KDEDATA'] = subst_vars(os.popen('kde-config --install data').read().strip()) + env['KDEMODULE']= subst_vars(os.popen('kde-config --install module').read().strip()) + env['KDELOCALE']= subst_vars(os.popen('kde-config --install locale').read().strip()) + env['KDEDOC'] = subst_vars( debian_fix(os.popen('kde-config --install html').read().strip()) ) + env['KDEKCFG'] = subst_vars(os.popen('kde-config --install kcfg').read().strip()) + env['KDEXDG'] = subst_vars(os.popen('kde-config --install xdgdata-apps').read().strip()) + env['KDEMENU'] = subst_vars(os.popen('kde-config --install apps').read().strip()) + env['KDEMIME'] = subst_vars(os.popen('kde-config --install mime').read().strip()) + env['KDEICONS'] = subst_vars(os.popen('kde-config --install icon').read().strip()) + env['KDESERV'] = subst_vars(os.popen('kde-config --install services').read().strip()) + else: + # the user has given no prefix, install as a normal kde app + env['PREFIX'] = os.popen('kde-config --prefix').read().strip() + + env['KDEBIN'] = os.popen('kde-config --expandvars --install exe').read().strip() + env['KDEAPPS'] = os.popen('kde-config --expandvars --install apps').read().strip() + env['KDEDATA'] = os.popen('kde-config --expandvars --install data').read().strip() + env['KDEMODULE']= os.popen('kde-config --expandvars --install module').read().strip() + env['KDELOCALE']= os.popen('kde-config --expandvars --install locale').read().strip() + env['KDEDOC'] = os.popen('kde-config --expandvars --install html').read().strip() + env['KDEKCFG'] = os.popen('kde-config --expandvars --install kcfg').read().strip() + env['KDEXDG'] = os.popen('kde-config --expandvars --install xdgdata-apps').read().strip() + env['KDEMENU'] = os.popen('kde-config --expandvars --install apps').read().strip() + env['KDEMIME'] = os.popen('kde-config --expandvars --install mime').read().strip() + env['KDEICONS'] = os.popen('kde-config --expandvars --install icon').read().strip() + env['KDESERV'] = os.popen('kde-config --expandvars --install services').read().strip() + + env['QTPLUGINS']=os.popen('kde-config --expandvars --install qtplugins').read().strip() + + ## kde libs and includes + env['KDEINCLUDEPATH']= kdeincludes + if not kdelibs: + kdelibs = os.popen('kde-config --expandvars --install lib').read().strip() + env['KDELIBPATH']= kdelibs + + ## qt libs and includes + env['QTINCLUDEPATH']= qtincludes + if not qtlibs: + qtlibs = qtdir+ "/lib" + env['QTLIBPATH']= qtlibs + + +def generate(env): + """"Set up the qt and kde environment and builders - the moc part is difficult to understand """ + + env.Help(""" +"""+BOLD+ +"""*** KDE options *** +-------------------""" ++NORMAL+""" +"""+BOLD+"""* prefix """+NORMAL+""": base install path, ie: /usr/local +"""+BOLD+"""* execprefix """+NORMAL+""": install path for binaries, ie: /usr/bin +"""+BOLD+"""* datadir """+NORMAL+""": install path for the data, ie: /usr/local/share +"""+BOLD+"""* libdir """+NORMAL+""": install path for the libs, ie: /usr/lib +"""+BOLD+"""* libsuffix """+NORMAL+""": suffix of libraries on amd64, ie: 64, 32 +"""+BOLD+"""* kdeincludes"""+NORMAL+""": path to the kde includes (/usr/include/kde on debian, ...) +"""+BOLD+"""* qtincludes """+NORMAL+""": same punishment, for qt includes (/usr/include/qt on debian, ...) +"""+BOLD+"""* kdelibs """+NORMAL+""": path to the kde libs, for linking the programs +"""+BOLD+"""* qtlibs """+NORMAL+""": same punishment, for qt libraries +ie: """+BOLD+"""scons configure libdir=/usr/local/lib qtincludes=/usr/include/qt +"""+NORMAL) + + import os.path + import re + + import SCons.Defaults + import SCons.Tool + import SCons.Util + + ui_extensions = [".ui", ".Ui", ".UI"] + header_extensions = [".h", ".hxx", ".hpp", ".hh", ".H", ".HH"] + source_extensions = [".cpp", ".cxx", ".cc", ".CPP", ".CXX", ".CC"] + + def find_file(filename, paths, node_factory): + retval = None + for dir in paths: + node = node_factory(filename, dir) + if node.rexists(): + return node + return None + + class _Metasources: + """ Callable class, which works as an emitter for Programs, SharedLibraries + and StaticLibraries.""" + + def __init__(self, objBuilderName): + self.objBuilderName = objBuilderName + + def __call__(self, target, source, env): + """ Smart autoscan function. Gets the list of objects for the Program + or Lib. Adds objects and builders for the special qt files. """ + try: + if int(env.subst('$QT_AUTOSCAN')) == 0: + return target, source + except ValueError: + pass + + try: + qtdebug = int(env.subst('$QT_DEBUG')) + except ValueError: + qtdebug = 0 + + # some shortcuts used in the scanner + FS = SCons.Node.FS.default_fs + splitext = SCons.Util.splitext + objBuilder = getattr(env, self.objBuilderName) + + # some regular expressions: + # Q_OBJECT detection + q_object_search = re.compile(r'[^A-Za-z0-9]Q_OBJECT[^A-Za-z0-9]') + + # cxx and c comment 'eater' + #comment = re.compile(r'(//.*)|(/\*(([^*])|(\*[^/]))*\*/)') + # CW: something must be wrong with the regexp. See also bug #998222 + # CURRENTLY THERE IS NO TEST CASE FOR THAT + + # The following is kind of hacky to get builders working properly (FIXME) + objBuilderEnv = objBuilder.env + objBuilder.env = env + mocBuilderEnv = env.Moc.env + env.Moc.env = env + + # make a deep copy for the result; MocH objects will be appended + out_sources = source[:] + + for obj in source: + if not obj.has_builder(): + # binary obj file provided + if qtdebug: + print "scons: qt: '%s' seems to be a binary. Discarded." % str(obj) + continue + cpp = obj.sources[0] + if not splitext(str(cpp))[1] in source_extensions: + if qtdebug: + print "scons: qt: '%s' is no cxx file. Discarded." % str(cpp) + # c or fortran source + continue + #cpp_contents = comment.sub('', cpp.get_contents()) + cpp_contents = cpp.get_contents() + + h = None + ui = None + + for ui_ext in ui_extensions: + # try to find the ui file in the corresponding source directory + uiname = splitext(cpp.name)[0] + ui_ext + ui = find_file(uiname, (cpp.get_dir(),), FS.File) + if ui: + if qtdebug: + print "scons: qt: found .ui file of header" #% (str(h), str(cpp)) + #h_contents = comment.sub('', h.get_contents()) + break + + # if we have a .ui file, do not continue, it is automatically handled by Uic + if ui: + continue + + for h_ext in header_extensions: + # try to find the header file in the corresponding source + # directory + hname = splitext(cpp.name)[0] + h_ext + h = find_file(hname, (cpp.get_dir(),), FS.File) + if h: + if qtdebug: + print "scons: qt: Scanning '%s' (header of '%s')" % (str(h), str(cpp)) + #h_contents = comment.sub('', h.get_contents()) + h_contents = h.get_contents() + break + + if not h and qtdebug: + print "scons: qt: no header for '%s'." % (str(cpp)) + if h and q_object_search.search(h_contents): + # h file with the Q_OBJECT macro found -> add .moc or _moc.cpp file + moc_cpp = None + + if env.has_key('NOMOCSCAN'): + moc_cpp = env.Moc(h) + else: + reg = '\n\s*#include\s+"'+splitext(cpp.name)[0]+'.moc"' + meta_object_search = re.compile(reg) + if meta_object_search.search(cpp_contents): + moc_cpp = env.Moc(h) + else: + moc_cpp = env.Moccpp(h) + moc_o = objBuilder(moc_cpp) + out_sources.append(moc_o) + if qtdebug: + print "scons: qt: found Q_OBJECT macro in '%s', moc'ing to '%s'" % (str(h), str(moc_cpp[0])) + + if cpp and q_object_search.search(cpp_contents): + print "error, bksys cannot handle cpp files with Q_OBJECT classes" + print "if you are sure this is a feature worth the effort, " + print "report this to the authors tnagyemail-mail yahoo.fr" + + # restore the original env attributes (FIXME) + objBuilder.env = objBuilderEnv + env.Moc.env = mocBuilderEnv + + return (target, out_sources) + + MetasourcesShared = _Metasources('SharedObject') + MetasourcesStatic = _Metasources('StaticObject') + + CLVar = SCons.Util.CLVar + splitext = SCons.Util.splitext + Builder = SCons.Builder.Builder + + # Detect the environment - replaces ./configure implicitely + # and store the options into a cache + from SCons.Options import Options + opts = Options('kde.cache.py') + opts.AddOptions( + ( 'QTDIR', 'root of qt directory' ), + ( 'QTLIBPATH', 'path to the qt libraries' ), + ( 'QTINCLUDEPATH', 'path to the qt includes' ), + ( 'QT_UIC', 'moc directory'), + ( 'QT_MOC', 'moc executable command'), + ( 'QTPLUGINS', 'uic executable command'), + ( 'KDEDIR', 'root of kde directory' ), + ( 'KDELIBPATH', 'path to the kde libs' ), + ( 'KDEINCLUDEPATH', 'path to the kde includes' ), + + ( 'PREFIX', 'root of the program installation'), + + ( 'KDEBIN', 'installation path of the kde binaries'), + ( 'KDEMODULE', 'installation path of the parts and libs'), + ( 'KDEAPPS', ''), + ( 'KDEDATA', 'installation path of the application data'), + ( 'KDELOCALE', ''), + ( 'KDEDOC', 'installation path of the application documentation'), + ( 'KDEKCFG', 'installation path of the .kcfg files'), + ( 'KDEXDG', 'installation path of the service types'), + ( 'KDEMENU', ''), + ( 'KDEMIME', 'installation path of to the mimetypes'), + ( 'KDEICONS', ''), + ( 'KDESERV', ''), + ) + opts.Update(env) + + # reconfigure when things are missing + if 'configure' in env['TARGS'] or not env.has_key('QTDIR') or not env.has_key('KDEDIR'): + detect_kde(env) + + # finally save the configuration + opts.Save('kde.cache.py', env) + + ## set default variables, one can override them in sconscript files + env.Append(CXXFLAGS = ['-I'+env['KDEINCLUDEPATH'], '-I'+env['QTINCLUDEPATH'] ]) + env.Append(LIBPATH = [env['KDELIBPATH'], env['QTLIBPATH'] ]) + + env['STATIC_AND_SHARED_OBJECTS_ARE_THE_SAME'] = 1 + + env['QT_AUTOSCAN'] = 1 + env['QT_DEBUG'] = 0 + + env['QT_UIC_HFLAGS'] = '-L $QTPLUGINS -nounload' + env['QT_UIC_CFLAGS'] = '$QT_UIC_HFLAGS -tr tr2i18n' + env['QT_LIBS'] = 'qt-mt' + + env['LIBTOOL_FLAGS'] = '--silent --mode=compile --tag=CXX' + + env['QT_UICIMPLPREFIX'] = '' + env['QT_UICIMPLSUFFIX'] = '.cpp' + env['QT_MOCHPREFIX'] = '' + env['QT_MOCHSUFFIX'] = '.moc' + env['KDE_KCFG_IMPLPREFIX'] = '' + env['KDE_KCFG_IMPL_HSUFFIX'] = '.h' + env['KDE_KCFG_IMPL_CSUFFIX'] = '.cpp' + env['KDE_SKEL_IMPL_SUFFIX'] = '.skel' + env['MEINPROC'] = 'meinproc' + env['MSGFMT'] = 'msgfmt' + + + ###### ui file processing + def uicGenerator(target, source, env, for_signature): + act=[] + act.append('$QT_UIC $QT_UIC_HFLAGS -o '+target[0].path+' '+source[0].path) + act.append('rm -f ' +target[1].path) + act.append('echo \'#include \' >> '+target[1].path) + act.append('echo \'#include \' >> '+target[1].path) + act.append('$QT_UIC $QT_UIC_CFLAGS -impl '+target[0].path+' -o '+target[1].path+'.tmp '+source[0].path) + act.append('cat '+target[1].path+'.tmp >> '+target[1].path) + act.append('rm -f '+target[1].path+'.tmp') + act.append('echo \'#include "' + target[2].name + '"\' >> '+target[1].path) + act.append('$QT_MOC -o '+target[2].path+' '+target[0].path) + return act + + def uicEmitter(target, source, env): + adjustixes = SCons.Util.adjustixes + bs = SCons.Util.splitext(str(source[0].name))[0] + bs = os.path.join(str(target[0].get_dir()),bs) + # first target is automatically added by builder (.h file) + if len(target) < 2: + # second target is .cpp file + target.append(adjustixes(bs, + env.subst('$QT_UICIMPLPREFIX'), + env.subst('$QT_UICIMPLSUFFIX'))) + if len(target) < 3: + # third target is .moc file + target.append(adjustixes(bs, + env.subst('$QT_MOCHPREFIX'), + env.subst('$QT_MOCHSUFFIX'))) + return target, source + + UIC_BUILDER = Builder( + generator = uicGenerator, + emitter = uicEmitter, + suffix = '.h', + src_suffix = '.ui' ) + + ###### moc file processing + env['QT_MOCCOM'] = ('$QT_MOC -o ${TARGETS[0]} $SOURCE') + + MOC_BUILDER = Builder( + action = '$QT_MOCCOM', + suffix = '.moc', + src_suffix = '.h' ) + + MOCCPP_BUILDER = Builder( + action = '$QT_MOCCOM', + suffix = '_moc.cpp', + src_suffix = '.h' ) + + ###### kcfg file processing + def kcfgGenerator(target, source, env, for_signature): + act=[] + act.append('kconfig_compiler -d'+str(source[0].get_dir())+' '+source[1].path+' '+source[0].path) + return act + + def kcfgEmitter(target, source, env): + adjustixes = SCons.Util.adjustixes + bs = SCons.Util.splitext(str(source[0].name))[0] + bs = os.path.join(str(target[0].get_dir()),bs) + # first target is automatically added by builder (.h file) + if len(target) < 2: + # second target is .cpp file + target.append(adjustixes(bs, env.subst('$KDE_KCFG_IMPLPREFIX'), env.subst('$KDE_KCFG_IMPL_CSUFFIX'))) + + # find_file(kcfgfile, (source[0].get_dir(),) ,SCons.Node.FS.default_fs) + if len(source) <2: + if not os.path.isfile(str(source[0])): + print RED+'kcfg file given'+str(source[0])+' does not exist !'+NORMAL + return target, source + kfcgfilename = "" + kcfgFileDeclRx = re.compile("^[fF]ile\s*=\s*(.+)\s*$") + for line in file(str(source[0]), "r").readlines(): + match = kcfgFileDeclRx.match(line.strip()) + if match: + kcfgfilename = match.group(1) + break + source.append( str(source[0].get_dir())+'/'+kcfgfilename ) + return target, source + + KCFG_BUILDER = Builder( + generator = kcfgGenerator, + emitter = kcfgEmitter, + suffix = '.h', + src_suffix = '.kcfgc' ) + + ###### dcop processing + def dcopGenerator(target, source, env, for_signature): + act=[] + act.append('dcopidl '+source[0].path+' > '+target[1].path+'|| ( rm -f '+target[1].path+' ; false )') + act.append('dcopidl2cpp --c++-suffix cpp --no-signals --no-stub '+target[1].path) + return act + + def dcopEmitter(target, source, env): + bs = SCons.Util.splitext(str(source[0].name))[0] + bs = os.path.join(str(target[0].get_dir()),bs) + target.append(bs+'.kidl') + #target.append(bs+'_skel.cpp') + return target, source + + DCOP_BUILDER = Builder( + generator = dcopGenerator, + emitter = dcopEmitter, + suffix = '_skel.cpp', + src_suffix = '.h' ) + + ###### documentation (meinproc) processing + MEINPROC_BUILDER = Builder( + action = '$MEINPROC --check --cache $TARGET $SOURCE', + suffix = '.cache.bz2', + src_suffix = '.docbook' ) + + ###### translation files builder + TRANSFILES_BUILDER = Builder( + action = '$MSGFMT $SOURCE -o $TARGET', + suffix = '.gmo', + src_suffix = '.po' ) + + ###### libtool file builder + def laGenerator(target, source, env, for_signature): + act=[] + act.append('echo "dlname=\''+source[0].name+'\'" > '+target[0].path) + act.append('echo "library_names=\''+source[0].name+' '+source[0].name+' '+source[0].name+'\'" >> '+target[0].path) + act.append('echo "old_library=\'\'">> '+target[0].path) + act.append('echo "dependency_libs=\'\'">> '+target[0].path) + act.append('echo "current=0">> '+target[0].path) + act.append('echo "age=0">> '+target[0].path) + act.append('echo "revision=0">> '+target[0].path) + act.append('echo "installed=yes">> '+target[0].path) + act.append('echo "shouldnotlink=no">> '+target[0].path) + act.append('echo "dlopen=\'\'">> '+target[0].path) + act.append('echo "dlpreopen=\'\'">> '+target[0].path) + act.append('echo "libdir=\''+env['KDEMODULE']+'\'" >> '+target[0].path) + return act + + LA_BUILDER = Builder( + generator = laGenerator, + suffix = '.la', + src_suffix = '.so' ) + +####### TODO : real libtool builder (but i hate libtool - ita) +# def libtoolGenerator(target, source, env, for_signature): +# act=[] +# act.append('libtool $LIBTOOL_FLAGS $CXX $CXXFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS -c -o '+target[0].path+' '+source[0].path) +# return act +# LIBTOOL_BUILDER = Builder( +# generator = libtoolGenerator, +# suffix = '.lo', +# src_suffix = '.cpp' ) + + ##### register the builders + env['BUILDERS']['Uic'] = UIC_BUILDER + env['BUILDERS']['Moc'] = MOC_BUILDER + env['BUILDERS']['Moccpp'] = MOCCPP_BUILDER + env['BUILDERS']['Dcop'] = DCOP_BUILDER + env['BUILDERS']['Kcfg'] = KCFG_BUILDER + env['BUILDERS']['LaFile'] = LA_BUILDER + #env['BUILDERS']['Libtool'] = LIBTOOL_BUILDER + env['BUILDERS']['Meinproc'] = MEINPROC_BUILDER + env['BUILDERS']['Transfiles'] = TRANSFILES_BUILDER + + static_obj, shared_obj = SCons.Tool.createObjBuilders(env) + static_obj.src_builder.append('Uic') + shared_obj.src_builder.append('Uic') + static_obj.src_builder.append('Kcfg') + shared_obj.src_builder.append('Kcfg') + static_obj.src_builder.append('LaFile') + shared_obj.src_builder.append('LaFile') + static_obj.src_builder.append('Meinproc') + shared_obj.src_builder.append('Meinproc') + static_obj.src_builder.append('Transfiles') + shared_obj.src_builder.append('Transfiles') + + ## find the files to moc, dcop, and link against kde and qt + env.AppendUnique(PROGEMITTER = [MetasourcesStatic], SHLIBEMITTER=[MetasourcesShared], LIBEMITTER =[MetasourcesStatic]) + + ########################################### + ## Handy helpers for building kde programs + ## You should not have to modify them .. + + import SCons.Util + skel_ext = [".skel", ".SKEL"] + def KDEfiles(target, source, env): + """ + Returns a list of files for scons (handles kde tricks like .skel) + It also makes custom checks against double includes like : ['file.ui', 'file.cpp'] + (file.cpp is already included because of file.ui) + """ + src=[] + ui_files=[] + kcfg_files=[] + skel_files=[] + other_files=[] + + # For each file, check wether it is a dcop file or not, and create the complete list of sources + for file in source: + bs = SCons.Util.splitext(file)[0] + ext = SCons.Util.splitext(file)[1] + if ext in skel_ext: + env.Dcop(bs+'.h') + src.append(bs+'_skel.cpp') + else: + src.append(file) + + if ext == '.ui': + ui_files.append(bs) + elif ext == '.kcfgc': + kcfg_files.append(bs) + elif ext == '.skel': + skel_files.append(bs) + else: + other_files.append(bs) + + # Now check against newbie errors + for file in ui_files: + for ofile in other_files: + if ofile == file: + print RED+"WARNING: You have included "+file+".ui and another file of the same prefix"+NORMAL + print "Files generated by uic (file.h, file.cpp must not be included" + for file in kcfg_files: + for ofile in other_files: + if ofile == file: + print RED+"WARNING: You have included "+file+".kcfg and another file of the same prefix"+NORMAL + print "Files generated by kconfig_compiler (settings.h, settings.cpp) must not be included" + #for file in skel_files: + # for ofile in other_files: + # if ofile == file: + # print RED+"WARNING: source contain "+file+".skel and another file of the same prefix"+NORMAL + # print "Files generated automatically from .skel file must not be included (file.h, file.idl)" + + return src + + # Special trick for installing rpms ... + env['DESTDIR']='' + if 'install' in env['TARGS'] and os.environ.has_key('DESTDIR'): + env['DESTDIR']=os.environ['DESTDIR']+'/' + print CYAN+'** Enabling DESTDIR for the project **' + NORMAL + env['DESTDIR'] + + def KDEinstall(path, file, lenv): + """ Quick wrapper """ + if 'install' in lenv['TARGS']: + lenv.Alias('install', lenv.Install( lenv['DESTDIR']+path, file ) ) + + def KDEinstallas(destfile, file, lenv): + """ Quick wrapper """ + if 'install' in lenv['TARGS']: + lenv.Alias('install', lenv.InstallAs( lenv['DESTDIR']+destfile, file ) ) + + def KDEprogram(target, source, lenv): + """ Makes a kde program + The program is installed except if one sets env['NOAUTOINSTALL'] """ + src = KDEfiles(target, source, lenv) + lenv.Program(target, src) + if not lenv.has_key('NOAUTOINSTALL'): + KDEinstall(env['KDEBIN'], target, lenv) + + def KDEshlib(target, source, lenv): + """ Makes a shared library for kde (.la file for klibloader) + The library is installed except if one sets env['NOAUTOINSTALL'] """ + src = KDEfiles(target, source, lenv) + lenv.SharedLibrary( target, src ) + lenv.LaFile( target, target+'.so' ) + if not lenv.has_key('NOAUTOINSTALL'): + KDEinstall(env['KDEMODULE'], target+'.so', lenv) + KDEinstall(env['KDEMODULE'], target+'.la', lenv) + + def KDEstaticlib(target, source, lenv): + """ Makes a static library for kde - in practice you should not use static libraries + 1. they take more memory than shared ones + 2. makefile.am needed it because of stupid limitations + (cannot handle sources in separate folders - takes extra processing) """ + src = KDEfiles(target, source, lenv) + lenv.StaticLibrary( target, src ) +# # do not install static libraries +# if not lenv.has_key('NOAUTOINSTALL'): +# KDEinstall(env['KDEMODULE'], target+'.a', lenv) + + def KDEaddlibs(libs, lenv): + """ Helper function """ + lenv.AppendUnique(LIBS = libs) + + def KDEaddpaths(paths, lenv): + """ Helper function """ + lenv.AppendUnique(CPPPATH = paths) + + def KDElang(transfiles, lenv): + """ Process translations (.po files) in a po/ dir """ + if not lenv['APPNAME']: + print "define lenv['APPNAME'] before using KDElang !!" + return + for lang in transfiles: + lenv.Transfiles( lang+'.po' ) + KDEinstallas( lenv['KDELOCALE']+'/'+lang+'/LC_MESSAGES/'+lenv['APPNAME']+'.mo', + lang+'.gmo', lenv ) + + def KDEdoc(lang, file, lenv): + """ Install the documentation """ + if not lenv['APPNAME']: + print "define lenv['APPNAME'] before using KDEdoc !!" + env.Exit(1) + KDEinstall( lenv['KDEDOC']+'/'+lang+'/'+lenv['APPNAME'], file, lenv ) + + # Export variables so that sconscripts in subdirectories can use them + env.Export("KDEprogram KDEshlib KDEaddpaths KDEaddlibs KDEinstall KDEinstallas KDElang KDEdoc") + diff --git a/scons/scons-mini.tar.bz2 b/scons/scons-mini.tar.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..0c1ce52ebd59ae92d9a03a288b1922110cc899b8 GIT binary patch literal 58214 zcmV)3K+C^ET4*^jL0KkKS)yFZQUUsA|NsC0|NsC0|NsC0|NsC0|Ng*;fGQ*i1_4As z4FHe=h)7{PyUqwVUNPuX=XYJZlx{dZ$JC?6~8PP1t$rs0nyJ zp5J-J<)iKH;ELXDd&0Mv>~`;U?>Xpn4HshW+-Xe!61T11xqA1na*w&kPrH2@H@B_g z8Bvo$On|SR+ukwnwCBL~?f`SM+4FFFy7zh4W>r*{v{7lJW}#Iofl7*`c4Pnm0007j z00000&;a=lBhs`0D{KG&6b6mkinkZndo$UX`fja7-1PeJ`kw7Ch3?SVn_la83pb#8 zOJ@7-`Fp;)z1`c|lI+@O0*VkT-#qo>?#P0Hb^sbufCKNCXTG`X>F;CewTHLe-ah-? z?``h(M(w@hhkdo*cb)IPZteHIyVsjNo4bv^_UG5$<>uSF*IAzX9X>-K6gR!iaKX2} z^7B)VJ=eYN^4{04d8^&`yS?1z=5G6p=G6>92DiI|ynBEi@tC^tYp+7{?(IA0x{<9r z?QA=}>u$Ea>%Pp`d%gF2-48Qm+Rz#_`eEzeH@>p%?!I-D>)zfs*w;`H6aWf)vE7xx z+odap*lzgix0{3CXJ00df!iri_fpG zZrjoB)$>~&&!^qsbUGb3*bdJ?NT%JO9k)x&Fr5GZ(9o?trhDF7ceX~^SI|(+joV*S z?+c6CgWKJYVE1=k-jmu&0&Uzj&fa^|v^nnwpzVWKy0^Rl2iv%49{T_QV*9zzMw|`ZcUiSx`owMf++1a}7$1YyxU(=ee~~r_m87# zs8AYxcNhZSJDaIh7j<#P?Y36vHr*W#UhEEmoD6z800*{o`gC?D%=O=W*LH5+K>NEl zRju?lx)Pco5D*9i0Dze^OeO@$3`Ub=$xjM+)iz1yrjhBX>7yk5Q`7(e00gEYAc&e1 z5Y%KT;6{}FRDJ{n4=L&fp3;T{0LW&isL7xVKmY&$5;Y`3n2Mg!svb{N9t|Y@G!0Pw zHBTlK@{EsD)Y^fe>8a%gLuzd!Ali(Y001&H^HdUPBoILaz(7w?Df*}SDtV%9DdI<@ z*qWYE>HrT-L7?<%dV@nojRt@K009a}f&eCfm=i!ohJ`koG$u_^v6Vd3o;4@wLqkml zntqaK$lj=800004nn@C#5S~e(Q^?fHdqhuE@lQcLGHMg_gGl{E7zAJe7y!^ln3wcL%O$H^Zdp2Xq!-U@;^}2xu5)3aX?6N(o62B`Fb9 zr4b51C03PzMbDFA6oNkB|YP=q955GE0Y0!CF45)wfX9;dJh1_4?Hp?YNZ`)g$S z{r|&+P?jT zq=aoTcw{KzIVY`Y1S08p_%G*F<8p@Sz+Ny-=DcmIxtI=+PRjaBzf z%{8q& z2!)5theY3$f8$cll#rK2=K=1XnfO+m5DX49IrJu^{C^0qFtA+9ZlKQ(?*Ztq?=A>jIBJQ$F!3y~ zBFXOaQCDhs-u!1o4Ig4kugBQ?>l5F4SdcBP&3z{ItkILGnK`&lrOOtPeVpmw=0ZCr zlG9BHx@yjFPw#Yn)SWo5wc}0l&P?f{^H4gno~}tU1(2HP!r2AH=gRJN^zZd;$2ZBc zutAS<7F+6Jf={xCuQ}vA+3s*yk0h}rPPs&9Ul7hGA6I#!=+)!BWKh_qB;7omWZgP@ zxw#gV?My(V)8$_rGIEE^aJ@6*xiGGTsXa-lbt$4^0`Cr^q9qTH*NgqYsCnWCNAaXf8XE>hUH;2k| z69U;B1ldTb^C5`8yfb9y>dc6CPW9e*K@3o{?6M@&N zc9fcG5yTCX`1d(|osTRPJ?kl%9r{)Nd*@mAOLr_$}uI!@}6!@oYS?FQ3Vu91HVv2WDpZ|X* z-93BeZste}-AP76C#1SF8Lw6@3P{>kl^rF4DDyrAt}SK%BGzPw6;6S+5c z598z4c7=(I?RIdtrcs?}ap6q|koW6Z=d|4`i7I_vfbgUpT^3o65`nb zG&FSF_dl<*!oK;O76N?!tnT=1aaiigE8|F67%P_fs2tpTZQXKnB{v|3Pr)2v(2c2| zbnmuNZ}6aTCjNx^(euLJUpXW(JqhegN0!7z525ZIX|6LQ`RiE%q&9mYE?&%;n!?3F zV6iRx8Q)Tewd?7#Lm=&#V+4N(&oJQ@b!5ehMWiJVM~hL2kq)#9MUgpeQ%v!hFCLY4 ztP&M{ti7=OH}6wuU3Vo1W(75DA4OigS+?A=V;$<1ol;RD!U0nrTw;R0(v^9#utqTt zQ+r$>B1r=M4XQ}tQMlvJ3d}}+8d-DSauZ22Ii!zXQ=YmxLkG|FRGprbk>#Gz!8GeS z#HFpC8HVr*X*Vh)|HdKPsygEo1rry4|npai2hj?5&L=*FSTLm1Z zkw(JNQ(3})efWDi*4!TIw|2=%BP}f?r#FPMf)vT2BV!)bk8ACDdnQ8V$~bS0`HfcFfk2O~F45hdKR^24ea@nU2kmg;@#C?}pRQEihQVjfH7Z*Ek-w zo%-4`O<-9+Idiu0={?~N|s1UE0*YfkaHWVH2X@h>&VHN_wTzT~B;ih=HUM)`+po|t` zPopm$t@!6#Z{=#HpIeZWJRp3lHuaq(Ci>n3+I_0*Rca?gH%UAyVna;_>+eG}`Q#1! zsB*){#?Df5t9mJtbdS|1X~`L=7&;^44p{}$8c>^NgS(4~e^P7rKf7E6lSpKkuG>~K zl)~{S4OKAYu$)5JI?&wx4Ar`|O$_atRlZG6P(rj0g3t9P_`~Dry}M6bod9`n2fYYgd~4CiYEnx^?+ilZ34=u}6u zEuqy6IAvRRV7Lxh4Qy)^H%iClN|Um8Oc@kWMD^3F#;g=M8xW~ptf;KCs6M)MW|`{R zoTWa(g-DC@w9$8WorzbXL+Z>^t_`H)8X?qMV&ov|)MM&oJ!}IySZwrWwXcqiB7$nj z9@un4-55+VK2qeQ757;Y^UMy3Re+uCV!;)BYLbjmOlw{vQAI0CQhF|W!jEmKr8z;h z%4X?3`YZ)@r11Q{?WT_=UtJGe;%g^QEX6);NTMgjE4?>io=({%Z78ti#?(KB%EN9cKCc~4J-)oJhX+iR zg2-FZCB7%Z|t>jN$5Mfk?Cw~1-FXadwW zc64U%=90awA+4EU<@gPbFtHIdh0FRM(z;@*sw$g1ecxtzrr?P;XiqfxJ3i!ZF3wGA zn6qku@L|}b4D8nG=Cx~xgfd87>-yQYQ$VIUl*U#$QkE|E&g9k$RTq*J#7}-uBBEfl{(AhpCnV&A7So z?GJkoi0sKKTQ;~q#dBXM9+YA2@5~PV`^qWcl_YI1)SI)-D9?%*E^}y5(5PqEQCGQF zs;Q`mR2^L$Vy@QbcFH2~`O8OCpgO;F>~V<7+Lu($PgB z%EAnclb|aMPhQlJ-zBkL6yo3`D6l$qI z4b(6S!cigYaSRd^rAXROJd<@0*V=6$dCdt*8WBirAW`gv8EuI9aBpHC1TQoju?(py zLb6DpZ|c`~{P+@o7pR1Uh7$P6{mM;0(&t`Nu3SEOQ_bv}hG`P=~LdCQ-=d)n5OU?-E1jmf>@J}!5~*IpWPcgk(1lbT}@O5HPe)3U#IJs?`z3M|lj zX#7@HaCLDLlh(=IH!5!(H?5<2nIq`pzL=_IXJw6Jb_5|CDAnmX}dKshP=5qZmA3%ZdGT{c?E+dRFWaCQoc9dIC_N`jIgQXoJ?0wROs`FQi! zEse*~A_xQGh!6-03;f7^7u?~3A)D3bLqM()0;#mW%_0jDDu(i?ISRC*CeDc5O@DT_ z*x3%(VbU5$Za8Yf1vD^{1R#u|Mkm)~fQBF-BZspL?D5b#Ou_WP8e~ugU`h6zG$;V} zFe3;%JuQLFWY|2^+t$$aq%8FwSPFTG1Ck65sJn{PNz#-oDP+8qJdM2By6_G?v;&)q zAuWQ!wW`+ZE<>5fJUZrlzM;DL`pSI$zItFZLTA62V&dc%=#dTNuC6C-n? z|Bn{w@qBwQcE@Wr6s!@y#WkeW?NfMW{mDG}&sLKmrd4GARG80C^2;V96p87u@4;ZT z@;?9;_CMHh+(hELXbO_?SM3W1D~c#a{AhdgEW|0Vm+pVWE@4gYEW z-{O=yM&4a-B_cH_#GEogd3u_U{p^A!`C4#A>p(tWX6{dv4*Y5T= zNOQsF<3@|-hK2bgdZl))FEXC5R8)MW(6kE4ojHD&OB_#L)*mzrwS?G1@g{bcwz^_@ zOsy!=1*2hznJ6D^R*=mhH^7HUw^Uw==y4Tf-ml+UhxGn$b8%ZUhkw5hHp$sLBV|dB ze*6&J99#D$*Skn!u;b-`Y#3n4lp{DpIulK3n4srVtE%E0iWOUI5*iKxy|Z()8^owT zA+8jH!n>QbM{fyoK-n;NL%}r8;fidm`0~sR^-a*;rG(*RqZwT}Go!Io#rJy&8OHg! z`hh&=i&P9lO{9Qjz_V`0q*ek}^^5Y(5upphVMe&QVh@_JX$Et9EmEr5r8^|nYUPu;yY*9zToGb7Wtr1-{G>J|^VWuaAt2ol| z6-{3d@tYCZdGyG<{y~bK=(M|m&3>bt4(2kt|7872FSPEEKP-oYYXS2? zw7>3uA(O4{3DeLVM@QKU6BS>EFFqt2K=t~=>VCZICwVN-~uRdFnfGybbk`}Aoi;tIR>?G zz$>xD^M2Q945E`qB;H69UtHekZv~ysc;{)KG~Xu_!yg}YnzKutxEQ7f^;x5bMz$rT zvc!{PgBlR2LHrqncNpXSZJ|LOe$O6b=V@v68*hZ|B#yX{K>+?O)$bqhPBYIO8@(F% zNDAoJt;NN>UIXVNpAFo2j>A8F>Wg?8Dle6k>YBIbmw#5>;AlY7Ov%(t=T^LwRR-7> zoaSaa9W*KDQpVCvD{dNUIV6`q4slY*M;;ufer;Vz8H^=#%_Q8@5=)|bBsVrbsjhq! zFrDVI-+T=q>L}GlGSJ{vK;tCAlu3l*4RBH(1{4fhj#f76fsPHphV|=tYd!V9Y|N#o zWqHj8To4b*I? zo0uh2s;sexwi!k$qQWa>UWKg%(lu3?w$!7oYukKN)gQ?4_E4N8Js?uRQ?h!Mfh@fG zY-{&H+*vYVc3Riz_VQ?dv_$hpC9F{TGq1;9i`SpKmU{W+wS_SRLP?9rmZ9lYR;sG1 zkq6l(@}akp?<*Z8pVq5pa z`scqpTqykkB-l{(aWp1JJ)R7jdEYt)vF`b8NW8DK$O~jb>u|~_92x>blfMbE=g5{Yca%PhJ@u?KFgu8LZCW1Lx{Py>DGU(o$tEGX9?p07 zNxp1rnjSeX@Lf&c9w|PcVP2uWF(WN2W(yS^ ziPPA&SbMo8YigXxv4%u^6>0;G-t`Msous%gF!3Zdnak7~W9bjlCE4tYq` zFcM5KlK_Ow#al!Z#EaIxb#vCPRB^uV?xRx^CYxK0$2v6GkdT;>^PHE3$&%B3ES!fe zC1-uT84ptqi8mC#hHjn7mpBP-TrluXMvP~l>q}U&o4;g7Z`E!mr%6z{Tio^6- za^P^~Jt!l9<%Oecf_#VvZpT<8sA^dWbBU!z0PigX`>68!Uo+WKv^3ui=Daz?uZ7%CM+_kia zOO;!0#yhtB5}sJG(jh@F$VT#S36F06MrvP4?A3oR#OjLk=L)FA4pI`L!ZV^AhEbT6 z3}q2v@J}6lU{ekA`>($bSF2f7GyIy+m$-18$^3P!^c<}akQ75ik0lCk4TkP{_452& zlc~=#chxNDHWC`CQmT9fR*f2}$>D7XRDTaTz9t?OReubp&wDg)_^jO2mPl_IKLa%K zd%pf_OzW2r;9416IyyyR!X&*`vHIz1gIF0*(){Q!t3EUXob~4Y^J6yXm4*UI?&&io z^dDAl6ckGXgxx1188G^OD<2gvy;kQE3$vRSbK|qhMn15x8mUX&tttz0Xs*Y9C zrG>NY`8>o2LMj5?_baa2S`_-QX{xJ*z=7Q+1XGZ97f1aw3blN9g;jDJ= z(WJF*vTpIz+fh-4hie7(WIXL=>Q$zxa;3=x_H@h$U=qnyp`9Y?wk-R&-zy-YMCaRP zCXh~j|1*cFe;J%^hXhqn(eErSGB!2_Lo&pjcH7ftc=*y7hYV6EBHC70xSG`lk_19R zZ)kL?v|OAonq_m!Y=_+fRQ{}&O}QnGE$59rI8qA`O-T}$Y_()>YRXKPiV_PEF+329 zo~>+*of4!vD+Q8Pd+rAjr8G5V>X}lz%&t%vI<0f-+Mi13*CF9i7d)thX6+QqGPz37 zg;(P@(nAe&HPujzs1G@J9^0M}Hh8-En>n&Z2zn_CfpQuLUsN@E$(|~PM62w*t&}`>(XRiZq3p7ikjF}CzerIKH(X%9q z)u5w}{{2AcZ+Cb4vtvQ0&9NkS=?&+2&9*oM-Oh-l0TxspB~Qh_%`Do|puBad|>rw*T!d;iYnJXV6NMw z*Wq5S?aA#x>@xae6u# z7;UQp^)zCk!8up1F0Q;()tb#iwN@a?;qkVWAG;-^bmEtOw^i?oA~t$rAmU0pm}DUI z7fQui<~3vV@p7~W6GgkA?SYD{xH_grWI`3x+wyQqvhq1OuL>1z{g!+0V9hz!jL)(>}4#AUgZn|sb>iUvx7!U%;?S_lfgKUL_= zA_ei{>eaELkh>8P%1BnsR<6X`=|WZbmM&G?f(GG`bP&>V=(_@2sALvKB1tkdZxsBSc-Lq@do9_K4%nOjr?J2_Ab>Bqr>aY8uJh?Xv@Z&gXA8NQ8Vfkohy(Q1&}Rl+e63oA5** zWCT)~qyu`HR3 zBpK@us`5J$rFS{!0_mlgICD)Pd+iNd-3}%cWsyn0z8vP}#NWNUY~^@y@4Ga@h+wVX znj;(|-`IhBXle?LCpC}-kvGT_O!-0MUOO<&Lay;)#m@}lfe9%@5k^G!b|w;Yjzpkj z6>z(FYuhABWidvxGY62E0%fsAg%k;}c1%qndWoKROTL8y>t?T5HC{R062?<*nl^Q{ z)zOH(cN#0%6c(!&LOAiCMoi-&@l)sTS$=C_-eOfXxlLUv7LrajY?D#hhc@As3mChZ zZ}r7NQXk%1L7LvHZ-V?De58-UX!guh2t`{?*G2vO(MDP}bL7W}UL^H9S`4+!dGATm z?MniNgzhvZiHM2P8HusU*ymBY6$ZD0Etu|PqsgfaZCvk!=4aE=FHBHDEr>Wn`n7f) z4Ez{YA(<2AXIS9z>AVyFS+?X1ZqPCH6%&Z}nCy24sZH1rb= z%fv~LoYP%Mj$V$~zZA~cI%v_Py$C4QEjBV+UE=h1C&ZSrmJpyKtm0#b5rPpqO4`x1 zXsR4m7B%nRX!7qCpOEa9yE;An1}q)LHY4ghB5X4U@}u;8u#e8T?fT{2>izi5S(f&4 z49*Ydid)Rn@lN)^jbaYFkWTY%l4slB2f9_Hy}qBmagoVx_G?2Q&d|>-Jd)+UvXaPx zU-o_Rwr~1d=-qzv{!O3u`gfH-8`EQVQsW;-Po4ZKt9jI~VX5DVH@$nP9qGYTN7}v$ zqB^ABp*c-NQ6r1pQKcUfa5CrdtBDBC9XRx&X6d3kwo2fcob@62y`(yHiaBS8!6Os< z^KY_E`VZmj2lJ-9%0n7M*_n0fbJL=|Q9K#{C(^io9*-~+2tfzf<9{Aq3N=JI@Dn{o zr3GF-A*3{uN|%obTu!4f$TM`7M4PMpxPG&^eVEAcm#`zzf zTBli`wfOEoO3V8ul)mRUj(PomG4vUFcNY@=?-P!me~Vpm*VP1lzk&&fl%Kz+-X~^a zX_XMrs=sG)kNTx8Ek!Op z9QR(^+kfVZ5I;Sze)k7rzNmOfK}!Ap*RDC!r`^lYljzp@5-&tdDCErndC2##$;G44 zmK%$45>RLLpVU-S^P>K$V3QfM_iH90^8B9<$$Y#temzdT(zl{_pSdIBg0RS~M@x zxh4&N8%ct9l}sR!L2&=;pTZxft)PEdCx`TJ0EIS5kp7O%G!hZicR~8q@W;*J<>i0J z^Z$?W8}@w8&!OXVF*oYG!WyBnZJaU@%EB=vw{{`fuyooV=yIPw#*8nA zu;6Ruu$?=09h6Y4Dup7;S~jQ+X${t5twOA6C5QrA>IDG7fRi%}1Ouu;|87zLz5j#9 zO8J>jREn7e1JG<7^mgxP+Gz7$>4%YFmK#_nxsx&+qSL^-EWXK^(vaWY;$;Q2fmDED zBO*sUymX(exxex>l&u8>O)%}PCWs;h-vt+-sEgoO8T}&3MjP=-jXv5MJ6>5QXvlDd zNv+Cf(>4QyKdK2fllA|l_-Mz`{okMVXZ?fkBmT4T|62$9%&Gp5#rtXac%Q-JEus7| zzwo^Om47}@Vt1EFyxZ5Kgsnf*_F6xK(}y2SoBUe-sGs^i&qRykR+IxttPE`K&%zjx;8yM6N2 zP|**Sc-`KwQQtp87kkI7k9|4onS z?%j0L?vH=fZS0jqk;r!LdoLq$&0mrI&o8ljyd76a`8fZJT|M7)dq11_e*^s5e4FIH zCy@+$gMvQ>jUK!5B@*+AC!0IN`@R$As{dV|@comKJa;{$rt$aOkDQ3-s6;e0*U=Khx~@{?E7c z+sEeidsKL7(`Dsl`@g-$KUqS!{15vl?%Vdndc66R;&M&P+9q>h%_C|0&B>eX;!n;j zK6Qinkm_>Ol4#CbW1!x{R5e2t#scU|{#2W53Q@Ybo=L|he|&n4`3io={xlB1&H3p$PK^9@{XC<&_C9-9 zIzDXBIN8nyJ%5*lyL8IYh)gK)C0zW<`o8C8$X>;@B1EJ!b29bfX2z))h6)wd36!6! zhqfcXta@^J@jlu3YuY#nM->uB0lZlNe{;9ApJ+bRg(%2BOu~I;kGB_E!8B;iYizs! zOC&6H{-r4@Kly*!nf#jAs%m6z+Sz#QF08{m#B~-7rC5~U=6-Lv$CmuL_`6eEtuGYB zf0&I!Sq?5r%$_x^UWrIjh2jLdc8*Cuu_^?w86psYoFowF^gLa|YAz z+-d(M$Er_OhMuS4n*}F32y~Q~bXo4$sTHG?LcfH|x;AMFfl#WxRf`VFHAKd0cB^Er zxhgP&_;6-7GOW(T=tDIlrHsf;m;&@LqByUo*XQNl_nhixa!lEpIjD*0s)Vit4N#*I z<8rnc!l;$zr}^FreeuqR->$ALe}?pG8q3j@{0(!h3xoIYVl%9BWw@K*R$R+T?X>2z zRYa%wWh>VfQx+-KmB8}Evx!9UrD=-C`Rxnstsz&9EG(t2N%MQco0lrn6QTsm+`Vg? zzfPK3=Xy;{ ze-5Vh*wt?*DEHgUH%-NtHfBFAXo5y%w-Rj3Pa16leN@XeEFj{ETIPiv#Gi2{X#3eL z%Ut8EO0c>*4j0o~=AWxi-0M>_iWi*kmo~(dtx_b`;PN{+j8L(N7g{DEkuAByCd`4Y6aIP1)L{KzuiW+w!p%(oGBley%B}5y?5RzN#9=g~KNr?$3m-|^=$iWO z;?-jd`SU>NSMhjmxdpO&zC4@u@7&>|2d*8KTEQ8vq2ZkH#gQbEO1hnKekG!D&C*kc zVmP3)VP%{_w%FS((ct8U-KJ9plG!VWPeh55;Wi_RPfhBb9Av7fbL>A( zHPZ#lpyO8VZqAaa;#LcF|CbBiXxWh^SZrg<1USNxA;WS;n zzSg{4Uc5hDPF-{G*6T!yVX0#hcCF)5CiE1>fdjcvjV)_X+@z!BUC^@;{qMS+R6h`U zE5Poz!SC+~ym|fJ``1l%+~=DfSjrx=MnzClL_6}UPi7OBt`fz*ZBipvzf#qimUKiM z>q`zDWGGFI&ADK3fpZeVre8i{-PZCE(^MKg5`^Z z4W~3tKJWM2r8TaO;`ZJb2RYA2f<#Q4Ep9lOW!SWKOzbNH7MM=QMm&KclNMr|;E#HZ zbSPzedwTPLim0&@mQ)5Fjw25acQvulJW6Kao!=U;wR{z+>$&u^?B>oJ;h<-#qh{@%vk~5wLgGROmgigqe+E%^FJ8hA5 zxx4+>)Wh&W)IQFN=IOSpCiHsG5?=h(C%S19^FhlKrRL;+M-4J8;vYCS5AmZD*{xh{ zr7}`!rn%oE0nFRFyBJA1O+;b868ii~YDuovOH3+aCn-?`e}8+bU6+NF>YIESL6eou z`B<@vGy?-G$rR78-LuGn*3(xt!=XN)ZRkK`KVkJD+7_3la<;*EcYSL$xqxY#| zYbr;py{Vk7BZu!QY`;xNC6B1gR=DLqMFC)iTg4?eRIv4(AEeRiEi{taidoNWuEb*x22=N&m5WP+Bkl1xt*gz9Ovk{&JP>O#-An}=Jrcn7mu6n-lvA)o|0+a9=u_z zeG47!YzsiDv=s2M3TgQ3Mqd7QJ}=f^3xl4m`p#i~yz%;0uKV9+rwp6S&n)VstA=rs#I)5~Cj2l`jT&go>Q=K6E}v38wI)$! zju>*txS6P~nJj8K#HD>HGU&~&J?)2vT7kXpc@GB8NLg#jcXFysviRG?EpdomOwuhH zk;B;3?}b5#-vp|o7IRuf(xa(ygC}aINnTbpViJ@YpS&&G|F3><( zu$)4!)@h)*%NLTHis{|m3kL2MGj3DgTGmUcAg*FE+dTSK32&O-=yk7?eznBhD^`MT z{Aq>6GNx~Pv1f*%?&B)Uo^r^_GT7c_6wFkpv!hl^wOl@B?))6~ zrAVk_np=8492^IH7QBj0KBfV-dvtSm= z8A*{O{D>`Q9ztEwPvvyh>p|upjh8TI!~@^)Oe^E7)iJ0>c^`+Bx3TPT{XPTFG z8>F>%omJb{Gq{?r_?6Q=-S}mcs_@0Ot>Rjit_>Jhu&V=o`Z)35ixuJz=_ z80a6~mlSd5?8@A`xh*j3Y_d2d<#hb35=G;dAI?>JRp(l#oga#-;mC^C$fN%RYNdDJ zwnvAz91iuqv6SG9Q8YSo?;Fj}4zkikYbAE(gN(bk^WQ7Kb}fY^sPwFyfhXalt2uez zl9CsCc1k%Gg5oDW1~lPXXFSxdbtNu)RZ-;KeI*>9e9dIj(v$C-i-tVyZ^0Ki`7H83 zkH>9t^2bZOrv2OXcjL)dY4*utD){(Yp-*P!JfEHEX~sMFX~zaEaq;C}YyA4>>6Zs? z2S**<-Tf18iO6zj<-t-pd}dF{55aX>680>3IW>)I8Stj2!4xi6Mbh$#^WM*qoLfbl zT+(@EYD$vB*BI?YC=1N53vhkltwk*PXXIv+@OhE0F=ykqc9!ayQ@Z@)NP6yKBhp6?mg^Ni~A{xTb8xq zpS;mYLJZdb;nO|s-|m*O#({z(Tt`72NSa-D~99HR} zAtWx)Rm{y?w}vM=K~Cc`nQ4fuS`PR>21k7InP2 z+qTWhc*jY0itX8S%D75Y=ZMRGm6Kz({dSmdg`0HmrE+q4M{PHboN8H1j8k18qe$D) z5v};wS-%uZYI0c$YMS3HiO{CbXV-W z#Ct6LYZO3xbd#c!bw$@h$h(PG~}l2?r5 zHEv+-zpkgL?{Rqg`1b5A-<~UKyO^e?Qq!xG#O7mN_(7o^M?2MZiuq&Rt*LLKlCcBjxaLG1c9$v+*vazCwBNzx9?q20{dbC2hYs1r=Dpl| zqQhYgL~`erWdBEPAXD1W+@5xULKmM6Nj0;gv&qkUwPZOl1lcnt@$b{ZeyiFobpB2# z9U4Q)iA+I5!bQSwIDQJV)*AM9YX~qMqGezHb$a|hHg5VmM0)>6=T${3`Yv*LcxThR z9^S8qUzIaI2S+{+B&w-Dbk6Th=jX0CZ~u0leLL->{K?89_i2wPd%w_1sr%3npO$W>N}o%@B3O~-Sx0iB-C|ZEyQjj{RLyz&Q#Q2q<7tVuG<@=zM$WU{Mx3YR z>gQco5$T=^QR$}$f?YVX&&D}J*RX>o77>eI%PHlDD!HA6)8)amV5U{zOFi$xz1xzu z6YG()iN}|sQJp~RJ3I|^VKgA_<77hwKAJX$UVYI(+`Kop=Pl};3MN@0dofs=GZ#*9 zSbEwO&N6eCLGO z+LNhjRTZzMB2|7~SDg6r%|yOltMrlB%&!x!eQPmb==_Qx??9V+CYl{1oJIHHV)J3F zgo-Ba<&@+)W4p+D!kxS8P1d05rxenR`SO!w(`!g_JM;f7M@94OYW;fR+a6=#v_Yb! z+;y)r&$~s^+^W&Xln8wwHi3_PS>}Vyk{{s z7Ko8sM~^NXZ(60_v)yL1 zc@b)Q9KGA+!v(T0CEGhnudlJ|H2*I7M@u!4gR|3~xe|E~wxr1PS;|PaH738NZI$td zt_>G`cgw!n;?PZKgtqb?YrSRkT#3p#_sas3#x1{d(}f-|e4) ztebu}rad#>SAF7~tm}&NBDwJIF~(cdTvGa-`wwaJjd@?}?W8{)4Oc^lNew9`dt~nX z;mBC1f`Lx0K&qD!Idt8f*sWe2r)DX#G0b^KyZ`@cYGkR8s9H2hA)EB6?oL6Du9)idz!YEO1Rd_#iM@-+G7Ez zUukAyfD}83AX?{~jRbIp9hyyzkpoCHLnY@YlNIsQswHD8d=s%(xgDLb!_LTXvo-Hd zh2FwBkF4d!L^pq69GWGpZOcXv3A<@cZ>A+asfvcJ6bx$=AIFBvgYT3HwY-B9JEn6p`U=;^4Ofq zUI}+(CC7k?LmN-Cu+HuY-I>!gz6ROe*oQrCPHiQo$qAcWd|$ryq(<^-s`7GJk)ump zWehS>eZ+nPdD#?3Jsr?>w&hCpsQao_+RQXIWCu|ap_<1lmyQ|psyHsOzIr17Ee9tg zDQ>iv(@N#ROpqKfImMyd3oS{MvPx~yH4%{~Y_Ar;Xoi{CNl6v29@`Kj6LXvf?IAJ> zB+^N2bk4vns4dg4`M&FgBq$h_K65K}Dd}&sGD)F4)$)xQmG3VK%^AJ5Zy7YsM>CEN zTLwjQ+YH)F@K%!NX+T$keLzjZnEOBbsRAQD=OOCuDk`IPk*d%4GVVa0XJHfh3tBB*`Hlvmon#8&_PZM_929By>bB*;DsDC(eFcvtuC6y`jM3yIr{l zcOA;N1xBY4rR#AaB29u+O(^kIr%AcK{)#hV$8~e#(+bZ{VD@Jv;E2HGU?~i0#AnNp zjV@TNq&=k@?P=9Pw3ts=*1LwJ_XOB~u@-P5ETyl7}jo(3Xp%C{Oe zQb$XaZxh0LjD>BzSm>}V=E1D5bD=no8yjrZh=qJb5Gqf)bEQz5s9OSMIrLDloj+a{ zo!eWM;d}7uN=*p3wRAhAES8zc>k%fQ5E6~A-+Hma<8p#9Z=Wlik-#K+6bv|-EmaG5O2^5x7E zF-wI-HGYVK5|DWx)QA8jN;l~tuierkEjNyNc-ijKj~UF}rN$vL@3&YA8Q-po~rs^Equ9Lwgb%Gqj2 zLgR?5Q%=1%pi#H5L8Im+u_ZLL(>*AEYm-0%*WL=KAB@WVDqod?7Yy`3y~Xa& z)u}tD@2?d91#Y?hQOpLQ|dFbl5q9{{%cL*9m9z;Cm@HU`B9Yx}f43mW- z>wX(a-7Gm|-=9iu2GE*TRSAe;XkL<5AquIxfep^r%*4wQ8sS9)k08Woh+{p5a6z+0 zQz(Z9v`nJuP+`!32vlG`oetrof35=qZVG6L9d#xP!0539YJxgs$w)e*p(Rt135dv` z({0^`W~E0zhF&kWEWvrGv5p2ZAZGYZWmVL`?k(qR}P z7`;xezd6=QlR=~?9=F!PWSBG510?G!v{yw`s&6p{(Wr|=pmapl61tx>p_5NR2yIa$ zI)a%sP*bS+mE#Tb#+m&SW{jLBA(~0hhvSZ zhau|iE6J!V@wu;{+8gab855D(Jt1(Dw?@v*3A^N($aZ(S9Pi&_jUnmo=zDZX2q7gH zMQsO(O?FBKHdvD7l_XK{i)WE_Gwp#g5=8?oU4i?&>sdP=U0WijUdz8J!;PfTtm;!0 zQIRR}yOo^?qKW(sSF*D=H(p;JO=7j?PgHPouRZ0CH45Kea>ClK?3n7AofKIGQ(k^j z4&E=e&4Mm!@=T>Ob~>R3om`|TAaOOymX|4m(QxM|Y^o5$R5ExyO+xi64RpjO3o+*v zS0tDtE_HGyb;4#Dj<&L739ZH9q-wQ_oZARVs_g98W+l(B#Wk!q*46xb>>>9FFkteNsg%Rgj%9Zq zzRni?#3d`c0o%_qs3$p=xt0LRIa(A~*UO_~K$`UU<|2xMap#o`kvR}7o=Mpj6Q+!5 zGPPw>Qq5gbrJ`1=EQpA+4qc$vkbch7i}IxK^R+ggAB?K0`5is4D(CB=(sSC;zTTc$ z(;QDjc@ctpt96M6JG<{Lo!1()f8X?xqD9e;{1i$bCCvdk~86a%@x^WFOCNDo|aCT zx_$p$_om^&1Y5oeEQLWwx_XpASaNJ*5Jm`VonX@S=y5d;9-3!8VS=d4>QOb&aY1A~ zUD8>DK$F&3U~N~YFAZx*czgJKCWw(Diy)N?0<>nrLXjs?@od|NKuUA(S*snAbFh) z9L4WFFI`H~<&J|00$ijwNi?9z9CMwodO;-8P;eW8Db}5<$7%aJbTeW9?@PU0c3WfAf*yq(yVP*Ejv;!A%z7SCO>;v)A$eh|89TkMsYd!TbjXgZ$b1rfO;%i6%(?r$4vw_r)mw4AS{Yg#Lnw5KMpS%fAI%+ANG_B0H@#NiZk z;Uzr-v=;mDmWg?**0pW!yQGyWpq4^ls4AqPDE-!77;?DgQmjEaKWzP+-)i}5%X!TF zKbw>BCq^>$xMEd8$z_|%D?AJqcRmU)mn}{OeBw;gXj!OxJ zD8$zQp_Sfh6G9G1BGRjf~cW|I=JE;FI;fpnET38)OhIeO70(lU|qN#)qm7}p}iRO zE{U-nQ_pAez5L314-7RAB=Yj?8QcfLUchXDQV>kXG!DPE_Uq+0^sJ|y?N^woH}af% z*jK$GMS^DKXq@sRrJhdrWJmpKosi~FaS~cnF4y}c_bM7Wx2XyKnhQeuS@95$w7FM7!3@*1g-j z>9lQX=NH1^5mx%qFHeM%C2P*odAqiV2W4$!+Ora#RJG(rDME>uj8Y^gm%X-3xPFo* z3B`L1Fcsia2+u6#X;za`C+JP{?G3GR{tKMo#7JKiL#@i1zG^zUOF$>q__M3J_VnoA zd@ZA;d_=m{*?r#ivhsCX@QRD>8Gu%_h2mwvz-Mgc@{VtL%~9kv)^gjfraIvQMw02U zHBDqTixU*38iOXCnXj>w+j=Sbj4$k6xVmHWU0nTK`P566YWQc4X8v_Hf{$81uR$f3$XZP$wJhqtA}N~>%9 z5^ywa&1#||;T{pR#HO6eB6F8XQc6(ZN75I2qoVB=%JVnGzI`J$>h6fP{?=W&vtgPU z;LfQR9*o?<^tV4FH~mT*Grir0%rv;_{?l4!tqG&Upk(K(y`kNZ9h}Ib@Q1j{95AdQ z;(%;v*O#2zLKRg8czKS5jHO-b>;_p|?V8(Irp@Nfy)=@HlW_`@vwL2NvjapJG?0X! zGp=!qwR+W%yLC%#A^vEt%|{8;6cnDA=##93^4|lBNOOd@x?wWymF$ILk{nW{d8Tiy zAY-BMNJ)kG7YK?Xx@>ycJ;|JT+$50MUNfb3u?aq;yjW9lw@jDIx^E?VFQz@Q+XEf= zSp)wxF^i0lJ3DkZZOz(y(zA4$v4_`yXRUdeERcfk z<)=lRJGNR5snJQRJuc(jQIv{8<0^92ELEP!vkJZ&n^HQ1e#Q)xqK3(n0Qf<4bw z@GcY{$PMhXi{0J(XIWdDQD;i-TdOXo*eY5oL=&!4c7`(pfIK7k(@P@+Z?>f7x zZ2<@JN>E6pJLGYlFKDjNK(4s$T4NM;vnMSC2@sGgMdDqhgg~#3txoG2X{IQMh>KNz z&pr$1;_He6(1oR|?4|~EME!LP5ZnlXMeym<8^4plW<8oIir&4-Qx&-l8? zB#(dQiFae8x@1%A_~nbC8uNRk5OsYkir2d3+C+MCp#;*awh0$?ZbRsqh(`H)&&<8H zHU2l9U)RZ&tjEgqS5q=tyZK&V{Zelj{|(}@Q!(wdR%7z|cv!um>#Mh=&kT(s)|F%? zha6WhP`za3;g;t^bAnEoSkfvH1o}9l-xs^j&tSHb;Hb; zy!$9wckZ~+XkO``zikRv#Rj_Fetm?*Gz1 z`2XDw>u>WnCH`)fs^De7{Yd92Nk58V_IOR<0O1Rh!R0Rx51Y>2{%G_OhW{tV%m>(S zrP}6M#&XNoBnJm-4O*%MGaqn+*3>ZFBS~3sSizaN?gzIi)UU4{XfdvqI8y%*bdK?}@3p0E2Wi6M2P5&d}TxCZHp zC7NEx33Fp^lo}2|AY|QWWe4$py_@&YpVGp;G9TxWBMxVW>b?%xIbEm3+oNy4haOJA zeg}~!9!A=aVz+71HIPo@1oXGtKytpXtjVjwZ@((_0X;exU_lbGt5e+59{X+WxLaeu^A&^Ce*OT>(8U5esn$)4&8Kb&mn>7_@8jUdQWKnBJM=0 z(da>mR!2Q+-ptx%o9G_re=}*}orz@g=oe-xoA2J_@4oP?rV4JVAjvrGSRe5_ck0Q) zss>oRK{VoT&%oqfOn&fmV=QPEjkX?Wt+SuII#~95YCn8grOm{khgu>c-M-0a)BY=V z6jf8B-W;YLOqCVdB|9ie1g;voJD5%^&&NK&J?AH0VomO+YuM*@hg-_*^>${sn7q2V zZf_gRuP@$V?$)>`KK*O^{~j%Ea^P>HK>G@nCT@_K-qD@oIXgNj90{<*oAzF?B1}=- zsujDUTpJ~He8+^Lv>eD(H<;*&H+nT?W#}QCCA;!yuiAeZ)aZtPMU~zjljXzvFoT|! zSsOx5O8pYOD2H#-dcd4Au^8AjM^GA35LNl_*>bP%UY+uZO(R~L%^)HO z2MD)6vYO|i71vVgU$Q47Ccng-uNac8X1E6r$leuc;o!L*F<`68xMBY!ycDNA|GD4d zX|^c74|BsEE1n(IX6~=fwR&I5;?41DN--Yicbw@)ucOPX|G#(OwD7i4DC;R+6YWgq zX4=wkw%g~MQ3O z)$(RU2x2hTA8h#DSgeWJ=+38Lk+4i^$V8deoVZGq*wR811h13`<(6bihv&SmUt%vS zJ;Ab+t6CzAM?yiD2jdQPFlNR7di*W4?iD)e#K{wBm{XsOccCDW#W7v$lTENU0t+Sb zarLicWn7@-#~uEJGGvnZ?JDhO?WWb7DVQqJ0(m*Vc+J+Qr8%ez(%0l+zhveejaCI6~XT6 z#yt-l;XC_!If*YaGw`-&L%P(U?WG6BpQM`774OQmRQB(rVW#{{jcAVy=Eb`G6_XX6 zF62y?1}Ca1Tp}79Q{I=*hw;0BeXP=2uTuZaL+pM?Q(_Q(8P6p1(z$uj|}lHdJL=hGuIs+ zhu(tEYM8N0sT>hb`+mj}5L_Z#Z6RSgy6IDXS{T zrxtpIGw9rFP+aL6*B4k)p~CHp-@h-fX{ndYC9cImGkhB~crL%#FX6LhrBP^FmM zU!1eq+rKO3vzfT~Js9MTYRz2O;ZWmb`KG&p3E9pW9bOjQS3$bzVveVyUn$3At_jjO=po?7tIwyd61PJe~>E5o6o z6&KA@Pp58N%X$J!rJHeOCuGR7beU1+M`+@q1s*)iAluF#;`gccBnR#Nzo`G0U!*=f z8bkMOnZuymGv=o$=7M{&c28R6JV<;5kUbw#Wb}5PKzm_IeI3sEN9axFwW|VCl5sR@ zbDqv&LWTrH8XU>;5%e2H3R zr}lk`cmI!L#EARZHaKm*k0*KUEl@CjetGGppVr%1N%!B}8X#YVxxUUb`SK(|)g36DHLC3?;E0jV2oVSd)Jeg$I!V`;f$%3j28tAD_j2>Dm3H>t-XFnA{T* z%rlsrV0k1Bjr4sYP)G=4gMD8UO;2NUVgNcNF#2`>!DXHcM9hbW!_G zs=KrBL--WSs{2`X{x#2$B$7!8L$mK$y-wxL4s~Yc*><~+aKCkwr#C~oXK^vvvdFBw zS~2DIeIFD<#6jotGJDwFG6V9RGuGCPX1;W$UI(*NV*~0u4IrV1d7Y6>Z*e(mDqB`n zws;#uM!M=l(>L$4(GF1`NqKx<8J{F^bW&nCl*+L<_X~+HZ+<&@ubALhF9%`we1N@#(>6DH(MZcKrH&pYFiU)fx z4_BHFr0RH_(>HkZZu<`8Jwx~(rLZVH9*<|a-SOIwQ@;a7EXfr|^9%I;ye}H6*tPpR zr=?eyK^5Nk%BtV!cUEI&*k=Cf8U9F2iMZzfY@t`~+DF({Ah1=K;b53u)?67zq~*o! zXwYYeh$ zlRS={4Ttp`(eCgXq0nwF!#Xm_#BRD?Vl@;?tN+_5rYKr z&|T)-joZ_H-(P)87Y%U1qO`+OnxHvOhq_OMTxx~PdoBJY9G~0rjaT)Lkb9TI@Nka# zywu&v&qi;VuaAlOd-ity(EEQt66J8hNC}zd^pbgBO`@oIFGrU{@cge%KDZyL_5?1H z4D$$V8bp^5SGR|&Q1I;vBO5J~eqFTj^|82p0}LNAyKrNI`WP7c0ilVrY;{KSznTVU zk@jfaiC7Z`dN5?)aKVp%xHx{NxI2+UsqN~(Jp*e(b_71)G-B3|M|uwFx4YAXR)ys9 ztJ1ps6gPh_QsHv+zbFrJAWufhq1N9YLG6lsMMKsfBuV&uRerwjTFmvmKRbr|gSO+i z$7#MUC#EobQcy;avq@>CjN*&G!Mlz|#F#T*GQS^xT~a5Y_>ZMSx$ZuW?!T;khr8<9 zPcPM2>st6`Ic)o9=%)|0#@>FB{UrN4czt2sbzgh{tPib~k{acZ`Szd{T!Ej4^kSZ) ziuryl+bOeJFUPInrV7Tb*?v}UA)D!6NQP%?-=032ly5>*H%9A({f+D3-p1SH#4#7h z$z(vzZX55VJzNRpWii%d4rKW-+mWrW?r72c| zsVNF-C>XFw82L4-l`6GRtsn&}V*si!XoE=#q^dM*s~T4jtg^@~RB{A=)L*nXV)Y3f z{!ypv`rivh8maF22wAuXvK+l8r z`I(tx(1&Cou-_o3sPf*--i$w^d4&GI%Z?TLmu>ym+U1fR-J8?RW)r?kuO7So#04WB2J`d%q(~RTzl?LtaVq5wxEh&Y9zgX!7*9?;+L*oI3S5<>l-v_*cT zek|pdTN@tU`u%6(zi5Xb+ogDCTiLtt71r3P@HPy@kTZTThG&x*1#9-Fv8cluh%7*i zrG)w?9`?zxSossL!&)u(23hwK@$P_}VfTjSda%eV=o1&_eW2av-U;R<3-#}>%6qyv zCXryzBzV6d;WI|Y*Kf`HyPINcgTthyB;XrGX^jek_=Qp3V0cd$$_oB`%Ed}@nU$+0 zFsgId$D?N24BW#pcr$w^G)Qj2!I&cPyK<&EH@$vkd|tc_HqT>vDW}RTWg>lu;E_ zRAvVU)z1d|#EfzL466G1JOTHrL-=((_?(2}>-jj6ohQ)`ffSH`)r<71b?oF~KUw{Q z59$7|fNONgFLQg=iFlff+BS$>eulr&FmPayeexQiUS0){G?0t+OK(%#$wGZWl#sK^ z8re-xt)6<=ykTuErkmQIS+50E&Uze4n->0Tg2HwR9H@@!s;6Q-{7Y}>&b@zhEjw*E zKVSRE=$YBK1S6AW7gcswfd7y{i50N^bGoVy%ec&(hG=9fG5fV#?*L7j=Ea6T5OqIq zappoJg)SI%x{|}3ozIiwLVh`k#~w*RU2XWEhnhIQcdDvZJXrq!^6jnB<{9&Nr!-0H zad@+FJ|%s)F>?mZkdtAi*&MsidP!3Z!N)_#-{f+rpG-$kc7IAg55-k6I5#2?UKuF~ z2-Pw;sEBt7Kz^CcS#j6*?xL!*aAR8hL18(*Xl`BYF(7UMp00bD@|%a`V#vjXb{!Me z8!Y9D`G$~+LP8Lgr=q=1SMXMe`*}SGSe4O@9ZI|#jFNWSh&LUe?QcoD`#*=#!_~ud&3uNk8V##I1jU9Rix>`AN~0jKNT?X^fnYiVUx9QQ z4I!r#6!yYDNGILo7#rqxz4wq&@!j zU#Dw(T2SpyJHnHf_2#PKYA~uY?uI#Eb zAeCS`^|8SvJ=XZnJs{5R7=8^|G}t1I<-=OS;F}>O-NWzKJaGH!Ux1Xek7qdmQ~OKs z*3@ia6IES1dG^QQ%sOCM%9bjU#Ny&}1jLkK;b&f_5`UfU;`rm8huv^m`=x&CEfG^q zQqb7Qd@N`^QIpf@a@rec8)6n3x`)g65&)0#@be7yN<~X-@r?wFqclyXYqX+5A7ysE zs8VQilFIpKN+TAcu&*YNoAgX>NaVTW7gy2czqgr%-upa#dj`=G1W8D=L=*`~QbNd5 zAA%q<$ckE0Cf`rq^mB&UzgLR))-WiPnu+-LiNs9qJSadE_HsB#nQ=3YU!Q-j$6A&@ zzPsiA2b_}g^7qu7-DRAedVAbCrVxedamaRYIE1~B|J)^%Gd1{gZuMeXaFwv^Ng~;b%EJ4`!%-UF&{m8Qi z`Va|(GWXBAN6+y0Y~%j-^jeeuSIC88wr+X3cmCWyZ{&89;+rA-$?{DzN{Schzpl+~ zw_SU3@uQhFxO67Jd$r9sn=R^$`B{mrvy{h1=*gGrr7T7_jQv|mq1lu z_WIkG(fv0+qYNatT3^~;A7}C6yHUBOuY^-LGo&d&G*_(w(+Wz2kTk@qQQ{BuKd8L< zDK=g@e!bBRe>`ncI#Lz8W zjfgb7{X5nBE*sq7;QnQaAuow$uQlf5-nHJ7CoLl8C>O>`IuIqUa&mlM#T*-aFE$dv zqvJj_ICXpJ5+zM9TCUzN{EhLt5=f|ldnb!tY zw*KvW>1pVr&!_0a2A|pG{2Fu4>Fa9p&I!L2v+X|h7?+Qf<&wm7w1`PJDZCz4iDBw0 z=KFREyT1uFt@(2u66Ai@BBQ`;UVBX~q)xJg3Hd{P<&v9)(AgqQhOg!P|5pF#y36j! zW80yjK4Y{ZQl9CXsyeRjWt-=FK!mVT7^kv^8Tt11ozK~C9ubSB=)QamR3AOTq1DI` z%!ef84(;E&M1rWf9LIkfl&4splAW8?HaUnIh~>8kKAke)aBSKg&+`%_nMnGXL@OZV zZM;7<0twA=L9^_kl3(qS7q{HN@!>uI?Lm;ogEoT&tNf6`zQ)Kz2OPnGvs?Mw ziy4wXd4$DVpiP@6GR>*^KXV6B8BC^4kZOB|MC3?NRV6_X`jc(=e#D;y|2McY=f#aO zKKlN%jSazvm&u8Irg0tIL0Y`h4k-DakinmQ)3@wwWH?Y^VjQ6{6iAUnr)H$n3j=tlBc40)=kU)V&RhOPG57Zt-e!1;4B&F_n8$+q`J@b9jyf~#t-AMbr zhwjg6E?u$IS(NN78?lr5e0GM9#}H=fGmJ}V!@Vs^k0bCID0!s!Cds7cCxL_;?9&TO zMNZM47zSoM6J+d6PTu{K;=#q_;mtj9iE>ruVdLuVJjB!vv1bXtvHfLvHC3&p>;*b{ z2vNypsbqcGjUkQ3yx$>Jy(w40H^qz!PWKe4<)p!X(BkcBOK&R@nI zyIuz*>hU{^WxNNZ^>08FP|{bC&A)1H^`01@d+;P7DfSXo=+FctD?ZG4lW$4DdQCpq zC@7xf>sbTb2ViT~^&ay2GiTH3+~@BfQARxn6?NBBKvjpR-8;BM-4327 znX;NJn;lJnWbNXK{}nF1v+9y@&^O-5U`TcjvK>vnW9;BIQT_@-q9a(-c?^_E>=n-w zI(u!T=uP{Mb{6>`#lC5vrobd7KBUJ3liRuT+&dnZtULoYhKityEJgwy!DoT&IC^^g zxcuKXNy*mPxK&&&k{xQ_&;Aeab70E`sL8V6?3D~pz(RoDjY7;=*5acLzJX~m7 z8Q9un(V~$h!+%(5sv+$R%N_zN&z;rG+q^g)!0C5)k1{*}b|F|H=mW^zUoag$DRuO5 zf_b~e5kEagS;jSpu{}qO(c4)?&bs#9Q$=9unq?KNvn=x*VY`6VEHZc-mcF{QH(k4` zlz?Q}64>F7gqgkL88;ST8%|bwc`*}nc!8tg9p=%z`+C_F6BRL&$QuU`%kAEb8C8tfI|n)M22MP9a1L5M zrM=ojMlezEEfh*3A|fqvFCyV(MuT>-36Mk}6#YX^nu!mNKpmW2b1GY?s z7pX<|mKMU1h**j}&AB%`on5MA3+@=0d#ABae{Kg}$B|mSXEg(qPN0a-2RnLAk$c}* zI4=-898*z2&I`p@xz%md+@n{#=0@iC6LP6$LS~yJwjGh5#;1uoz~nsLk8FM*;MS;0AYuOn3nBl@%sW&!0RO)G8VJX}tMm%3t=Y>TxTgWc$&r=&L!dc)kt&Ato{rgPfL zUUU^Mgc=b7)EwAL#suwdWb7o(xXqt5l3Y#=8?PKU4>$Ix=h?23!Nt37#^MvY5zVm9 z0FMO0Q34owi_x!W_aX+!plK!;o`9(-2_l0fF(HIbK9)O8{kJG0kz#QfWA!r647QaAF}L`G<{}4^`6R)V6Emu*TK4#HK3$OcJ>DJJ2t=Ftshn26nC zi;(--B7XXxu5mS?LcgOB;u=mkH`1C6Kg^+72LlK0e|{pUni85&+mXc1x3l7T z{^EmI!b<84-teV0ejI4H6h7kxEq*s!&lBLc)oExA}{4@1;JE505zs z4Qw25qMjRfM3R0lr1cz+f`*arB>zD{6fj9a5Hb{yed1dO!_>&b>y{;T-$fK9QtZC< z<_f2*Lt#$D4e}%jY&y3;G9EHz_Q)H1MEtYN`Qn-PS+>|5Wa6=aTnu??S_n~?@S21a zng#?IKsngJT+}u&#PtqfSv1HjqEEPShM2(7%$yR2r7$Q^>l08Qeko{*dOEl&e^|o4 z{kyr9_ndVN++HQNRbv1kfmA^t47<$ zZ?Gs}AR_lDzn3264Wf~t5Ax9TsL9%Z-Y)Ye|IEorKF0?TKP2I_8FMrS*zPc7ARC1LIid-p%R7@<3 zRi{IC7zYOo&lVeu#EC!ah1>x^6!|AZgt8uHDWu=@=?!hj$QYhuK|`Aj9$#@MQX8Un z9{zt&av!u`lh4z$zgJ40r(_d!(vMh3?sX6y)9@+ISfQvXGBr_ph1yj8IQnDtce?p! zj^WLK{a$mWC<25~q)Me|SY$$A5+;yoW=2_t0j3}*1{t5BHatF`A@cin6KH&H4tp)C zHEj*jX-wa~oDo-FyF@P@%(DN<7VROLV5rxdqN*8GX7Zp{`e2ZRs=vm(?WN5KXOqNK zbC=Nq%EXeWvfER{EJ;-tnsxEaxc&bF;(Hu-d~KzSEESzel%qA7Xta?bm3gM5i6MW? z?!s>&gW}Hu^Q5O(3Hb=9r6L+ppo#)gND4%Io?Qm(=az))mGRal|3du>6G%i=15Sfw z!Y-2+5q`a^Hu}bXVv4N;-N)j0>4WG!|Q)dw3uZrnBoRReV74~ zPMJwBKWW6oj_;VkBuO+85=}%9Lct7-#P^=EIy_JwH3LWt3J-_4^rzN$Y$n-@h6d;c zzfs80FQovJzpWnH`?&^#?sswttcN$Xtq{Cn)_5YaySo}aX02lvFWwGjVlIn0u%wv4 zcS9^8VS|jSg}HMk2C^Cv*}Drx5oOy(i~(q9$}Ced7cs_a)?=BV8EGbn%0h(+vZuS$ zY;FOx;%;#;)lh0@5FZs6+7dLWG)WLNGb*7-qY$(UO2SAWZ~&f<6TgW&-cyy3I%*1& zr0KC#pe2zlr4U*OR-#?7$gIi8C!FgtN=yoS>GuZ-SW=|KF_@%ax`Ey>Wcu*V-{OAh*)iiEl@G3O{yhi^y{bw05#B*Ot^lGdQc6gaf$R>6a04;w z3yhoew~~8ZHrvbzGhV?4IPg#D5$cxeFD`Rb9LwQkwNR?CG1F9Uc15Rhc%EbYP=i3idCOD^F(NF5ixB5R9awOd_4UtL zj?J_9{@+2vZTO&gLY^`-qe?)>m|l(gO)LB&@PDY?_W>N9V4hx4Q{7SAe+Q`HX5AIoDN>lU-DBD^=#7F?Wm?cCoBtnTe5Vp`JkE|qYR+%F<7O~|{SsBWT zE|nRvh3DrGZL>*t?qQ(b>?GWmWcf#k3d6&Jodt!BhkYMz-6T#o;P^2ae(6oN@SONn zp+|?KyL4pi>N6?oZcjIp1B{{4l!2??^SvF)DQ;$HFkY63;#Owby6@%esD2(m z^GBCEt(}xf?%KC3+gJy)e1{uQ=<BneC)DvC-eNJ$`-T@rz=CS00)ew8P1;oPU3RM-MS z4np;CrFHP`f-dqPQaUH`aj+fC*I6KLNt;y6dXr=~T3~V_bde@YkpzaIEYmqg0x(nn zw#bMWGXa4ifPqw$n$2kx#2mm)RH9;ss1Q>JLl(xytZ*GutXCvT2gw82EABhz55tLS zuC0wW2yKmWAyXLw$a;2LZ?Ucxtv?ot++l zeLhNHy+H}!c_I&+La511AnB4qtWel&ni5{8EB;H%}oZ5 zFC&z8a%?d(d39l#iWjA-$s__45o(f}7S3jqw8-9xP}(!r6I7_I3i$;PJk?Yk2?LCr zlU`x{_$Jq3+ZlhegtOWHUpEf7TJ>)rlgg?{OMO)|emO){ z5t1(gI$fLq1#e2L>>J6ixw2s&N6NzohVgAmc#R~T?;7p4?a|MHw&3rAsz~hXy=rjXQXLM*ZF)`kA&@wvX?)PIs>4Vv9&lNIOI zW3QH}2`tX@)SZNcjJ|EAn2^dKr7(b`f|!Uz5Jm{anNotvTTx<#sM0i1YZoRZC`?+Y z$}k!wj3oq^V-7YLK+NRK?X%+W!Ofhw~Y3>08PG?Oqyn6AWz zA*D1pZUpKBX>u|baS5qtNeXBtCa4-IS|TZ7D2S362&NGcM4uQ#&)3@bK2*#)ZJl+f zm7Ptl731aD4~d)Pgd=Hg(C2TNB@LAo9TvNXoOMyu()vq8?I#x&0q`QUu^N0n)x}Ba zIPR!Tb*AbH)l=HYjlhS=F(pC7pKh!^R&TsyZQ1*!(rH6{a$&O_2<0-kWl(@7d43bH z?(n|Z+pk=nh~v*2FymxuOhO34Yh#?7Gjrx)ivY)lWI-t#!~~Z{I}jCM}YGgWjY9Wr+i*2i0LoI*o~mSBY)`9i~l4p>7#ki-eIupX16WYmXKsXmB2oYa&ylZrNI#8M6_`HR6v2>Jqni zObaWayK1QZbbHsS%|bBB>+)giFJAO>>qYbBycn&LI_y-2!8|CGq}`PT655_cX-=H^ z=BKx8!N*C(vTB-IbfM?lNfNs%d$Q`ZX={q`k-*z5>sQd7+i6`jnq4GM5_O!ZRPqUk z=s_SLb`RFm5fYNX!kZOUQJ4lpp(bQT8ccD;;GR5ogrYYJb~X(pNbyJ*b|eJ>x)#%3 zueCaevV1gPHyI5KCT84-I`o0L z;6NB1z@2cWe|eAJ!_bMTQ{JgeDe1iG-AZOqQ@kT@sO?#b*IbK)?!n`sQ32Fc{%ZXz#vV?JEOMuWn2- zNI;TAtQ15A4Fv@h6tNUd6hTxp6ja1bRRcvy9Ojj3I2aiKQ&o|XNU;`T5}>OTM2l%u z6j4f&6~w|rQJ{jDs3@kYq6XBsRWLyeIRSZNq6oDCNkNoj12BytPzRK;%6)4(ftcb-zUJ0Gf8lCs8ONcK^M)g zgjk>}IOwGriQygH5Hd)UX&@Shb2p3)2}I}eMy-@iG{oOn%}_Uq>pz#TJFriLf%7N5 zgzn5R453972`fNyWs$TB3=tM)P$A0|1Xw3{>7Hre38q3A3|++zFxDEaBsr`08g?pRFXU+!o_EwT>_- zv1qJ^v3({x2KnkP^SAwvf+^S@@&!4(W4#PK{*}?W;pTD7yfNRgmk)WXg6Ql*T~cQ5 zAZ{(TpOp_8mQhb^vuz&?0KiGW+Z#lLQ!p0&fKzZp0BE{0ka+M9odYU3(j1Y2=(6e zbdk3cWV*4kWr3ov9o-nA$03ZSO@djosGUb}|w~RVXaI-Z# zTr1m4YlT%7u}OEkK@~mHl-+6MA4vwPqH3Tcbxif=j4W#xjh0ICI^&*+*4AG9n!EO4 zSRMGGlW^ZqIv#eSizJAy2zYul-;7ow7m2*_>uLoh4_qOK|sq1ze)XkX=5T&&!#GnMFgTpV83<-??;oK5-( z)){uLirrUC(doY(Z19x5nZ4L=mX-?5D(26G@>(lSCRY|BFI(NwYLwcOYbQ^n=DjtS z*!tb`VJRUTa=9RQt?vs`hrRC>aA|8$La$DOI>V*H7ox2dIODvf#l^#z$GNGbx36RF z^Sjo~+UB~1ORnL;9}x4&X_hlo3_?cv3pT6Dt6X&YEt^^ydgs%cH%jsex;=R2Y}2Z> zt_GuuxJkEsUD_h;8##Jx-fo!~l>_M2rB;#AhRdsHeI}KwXMx;DQ!P<#qpo$BJkYY! zYZmJn)pR`5sEwA&z-E6vS+k(*Wk)ZaqIu9A8Nz2Sn2>~BD^@gSd$>*|LO~ndBTK$C zHF0LGXtmb$SBIcp5HTA(ac7`rZ9Y+^KLa-5$wJT~8%@syo$1`o$-F2-cE)j<>w&&N zC3GPM0w%`(GpRJ1;7+{5g+%bw$=NRr52O(Yqg={x7=UG-VRqelW7v4A%CAeT5Th9` z?}TM!!c#huR?j;)r9mrg#!58Q(Sf?U#0rvl&hCXu=!T)Ol&igT3gdk4@*SPkoKMP8 zio|AyhHNGQxUv$O-)pHm!fG@NqFHAOc+Q7V!W*$YPNRaV$z^s?~?1jVD-P3ZZJ2H;y_`Y;%*snn?2}5Xu`xARO<;0vhw0C zmx)*shN#aSN+P;=YCz{Ht~S9~a|E$N+;A5VTWNRKT3uSSmdl_*x1IEW6o{f@pXcX(EaUs3>9wbJ#%H=5dF9_;>gBcgB|xH*M+h z-8bU2UeZT7V2!aHI#t48aw1IH8Dde9B|MbZ9fz4b`>gus7R2++o^;NKjiS6GuSKeK zfkQDV3|5Zy={si}&K~uw)Xc1^Lyu{Ae7B=ia@Qs4AAPc~(GCl}Pb@wPDb3mKNY@_Dk~KPQ6rNz4)dIOtczCdB{1x0)akJ8QBZ3wH6g!R9pE_V>`Al^z)XU6 zg_$x2pmBQ?avjkOSjYy;Z}Z%c7#Sdg352WpH4sn`VnCQ;3LKz-Kd^-`?v;6xhwvv@ z25%6ecA#K?g?kcNK-dm1;*gOtU?-R`9SG=7u|c_y_Iu-_2L_byLqvsC*-B;#3=t7= z0?8S~h8v*}1lG(I62wjc$rOC%`Z)Iaf%;9JhG1*+YzXO>cRYe1#2bN(mdh zj1n;;TySoVw;IyG?82E8EX*+!MB1lnb+y*97I&Z~JRrF*I_ovb)2JQByyJNF zr8!y6@*~8!YJrr@p#@z+h(=>UfD>*MW*h_vOf=gzuzAvin5gUzS0rAsJkN;r5Bd26 z#2zs~fDFQ_!w;0q!79)~j3U4hB9Tjn#1z}49gx^WIWQOygh5H*cLom9?b!zK20*xA z&LM;^iaT{LpdVkfi>x#9&uPs_gNTTPhur!1v_@}aK70~7M7fGNMN-PeV}@s)b&CMV zw6gh{VU|6;%;gAp0pBN&%Scc~LJ~}c z+JJvt1u#(r)K4CVXYU;Y)SMoDhd(_GvYXpwI$Hw~QH*lR3{?TQ>D9|J`$_c$8a@i4)9A@c?tQx|hP{dIa0|b&q1R<$h zT^0qUSzKUMNL(p`3ZXS+4L1^kh=w#6qLV@K@2u9$SR)ZqDTre$&d)(;)gc+6K;7Jh z7D+u-{$zBc5lU}&F0(D?*Nsgawv8r1<~7^=lQpT+uYgXSG;pzU)m2o`R!z3Jn#l@k zs5EO(wQUJ7S8f4ltVK0NX@+T3pR=ncC08-V>)5iV#_61bJ|KT;%K8xbdIK6#By2Dq zg9OAhr_!1OK7fK#^FYM0Dri2vscC{pWvMEHP&7Ls7|BT$1Nx8&4ghgOKpNuDh551q zl`W+k_wEX5vNqbPOl^@K75tM#36qg+);X6NVZ{Oo15x zRb%GBKT@#G$@Rf7BrpM7N{Vd$*K@qhA`}$lBuB~ zaE2{pF}tR1sgbS(GkvO#CaBXXR+u1E4lKE%+r3H!p&Atgt4u`#0_dx%w?t4Gc{H=#dYi1E0e~+9G-kPr$3-KDD+mHMRz9--aJn+MhP&!Y_oAp!(9teS_>W zX_YQpXliKD8E06Y!Ibt8Ll8~SL;%sM3Prnm@ZQU zE>K+DdN@utVu7U2IX7f2g(Xz%x#4R9=|nM&ai!Mzy0ctITUQHv;Oe&(Z*k+&#?CZ^ zDC?qi)4w6l93z-C=~2}}`=!yXEhOu{_=T{?LLSv2BrhrS0pK|<^+?`o%-}X1D=r%$ z!QS<8%>ijD$tw3bhL03;!LqdYUJT;fb=@lF8U)G7&TiG~GlQdB;i|}ztz?L}Ly8fI z>D6a9doPTQn4LJ6T|5%DW}fVB=yl4*Gf1%sb<{R(TDOLULOcIJs1lBbamPkpT3u*O znsbWSYPu7LMNLH)Y@|bb$iWspT-}q9DLw;cUiK z`QLGdTMu$Tu@YvAgsGB~c*2;Z+^M0)Q$lIThLsk<%o1r!Mg}tGA&AtFKt$9;kq_%Q z2FwW2Rf&bM7LdYds{kl~B1D4{EfkD3QZ+!4Gcy4Yu>g?-$Pkp0qXiWtwNoS|)DP34 zeQF-1P0jnkfx~uUB=z{5`%s%__@y{hCg`K~ww8qNfz%ac zYfjBmdO@oHWfg%s*G{($0v%N|!09kJ=gs#5wf=vx6l3{x&m*!$HjRadP(?ho=czOd zs70ZTw_rU*e2cC**8@gViMYUfPkw2&yxJ`2vk;<^Dh!RqgvrNMVro&vw#o|=1DaiP zMVNmDMva(jlVQM>2e@oAil@Q-Apf=(PfrN7cYRMxyekLKZ}3=j;0C-}BZ2 z%u2xhd-D1HIx=Kfq=%Mya8QobB1DP8P~gaFGDiz6fR$M&0HFpzfdrtWLXjA=1;q*q zcIVH1jp>g#F9@u^>i5QbLH&Tjk*DPe?}{`uPK_O=%hJThAC7P+J<`TVWPvLl1GfF zykQe)GkgkDR;5>o0x75j^gyoAfbRfpD)h>(NUqQkDM(U95P+osq4AVy6g^(p!~i++ z#ZCmPKmrPCpJzJw-$T6hiQ@p0Z#j_SenY7g-a_Tt z_L2L+8!!4v5X0E8pg%9ezh#dv2FsG-E-~ZC2A|9CUI>ma^~#tYZoudc_h|I5gZ_?a zu8kkkAem)2se|8MBb7j%F<_$y&c=B$}?-4P_W(BYipfn>X_`E zbw#OWyQ5^mEITJ+HgbAnDq2P#T+HFMZ99|LYnLE>4C)!x7_>^xvX&Vp|BT<*oA1*n z?Q6j%Y|@AC(w!JySZ2p(Ju1VdGdR~n9ryHRleZoHVbe&ikr^LV3{_4m6^#03?Uv<4 z30BPmrgx%*A+%)2CJ*G(IgiBLf~IL&p(q+;ii#>Ak_aduiG(Skf*6W|sw%0fq==#> zqKc>~5)x(xyG4p76OOHth3-6=g0#3=jyI8IbG7Le)rVSe`O#KXUN><%Wxw30F%(L-n9h#scF5Bvl<=SoOpl=*z z$Yf--p}2|{%`piID-up+#>-OZ(yNzL!2}gk9gBs7mN@83`F1mD+IH&P3}pSN%st3@ z9tRC?eJ)CrufQNU@bX=ZcN3?tIS$M6A>>$_xt+RT-DnmmNcRer2lc8BVM1OIDaEGZ zu01p?nGnns&O@+ttUcT@m`ViTL6w-iY+HGDIOD`Zt4rQ>{NsVim0WT%DmYlJ@MyC! z+T`Ez=#xV!kl2)pmVl?oJ%|LtW9be+Lt%`_)Bgo5N%Ba$#?6Kz$cTaf8 z-48L(a;U81R$0BPgO)(rDgHOvfx0k1W^cw&NPcu|yP(n&@%Be_heoAf*iCv_VYY;u zzKIVR==NcZ;5d$Gr4-3Lvh2F05n3cZQ@+81#|syMn2yrqA-Pz^lwu%EbdW&+VyZNT ztrelP+o;x3I3=LQbyZ~%VEV4kP+!k2fCmT@` zYc|oMW1B-7NPEDwfvOXsEVfvJ0w~*f$FxP*o&*jM5)AIZV8G+F3}#!PX*Q~tK;X)n zNl?+~Iu4Gm|3ANQ->FV)zR04(+B-K@3=7FNog$pGF97j_EU_^{$(ps>iVQ>r#nJ`Bq8EKIt zaA_JCOOz$HQmWusN}@(0Gfrf7xCm_^U9eC#fsmHM)lCo>AbbZFV&@zIRx%9gOA=wG zK3g6lOqhKw1YZXaMb;bKhBDTp+dckm`kkoG84xF|6c6_prJ!0Mi_PcZ$KjMY=LRJ? zJ{fMk7#; zZHd*ohp=9KM_$vl3u}o7ISQ!*7h&mqc3)#@}w_~OUfM0~+e@uNu@VgP|Q?jC2 zk!3RwaNvFp?0=?>)b0CBJM z4+o=UH%#3O5eRo+4S;Wn1!hFU7Rx65-x0nF4A%cXe2pO73zy(2?j5RGouEW*3 zA-XG>Zqi0ET~CL;*4;m0jej2+yEsa}#2_Ek2eu-R`;XWulAp;Wl2VeuZQwi&2FOAt zz$>iS(Ha#rsQn*Mm`MF7Vj%xQzdz^t(Jret#46vGlVWlbwQbHAE#|&gprCdf@ zFmWH`80P`5J9h>c5%k6^WLu%d2_y(pwLZc-flu(_frlYO+J_Igl0$O?5y0{)i2{7s zsbf$UECE3K$tSPa@VDywzfQhU+SBn|l*ga6;bmGCZmiYZ7vl$3^g6=L-KTVxD=!jP zmxfcQhEOf)pF7sJ2?EimOtM*W#oD^gS&orSWt2^mOmqTbKdM-Xz9z3AhnB^5EvIym ztgy$G7E)J#oV-Rm$knA*TpUDdCl^7*%+{i{a?|4UjR{yLI(o}ABqCysAtYlKoT#yy zF5v95T|@REdt25mSaH*oI1%9knn`wx$V8%1nrb5HXZUiETEz%9OFeGd42J zR9v@ID5^FO2;C7d3dEyfWIJn(W| zs}?OGHajlv%$QnN7?kBWCesU4f$9*x8Jlm!&Go^%!O~Kn8&lUOX%9RBouu@W(W1zE zs|q%gBM}DJgAs-hta33R(6ZSjCXz!(1ij%opas|k0fF@O`p1*~1^uUjn<9QBiv*&i0q#TXgknhuh6aR~5@14+S(Okd0D(w$U-M8!5Y1>PhzcZ# zVkG(E>0pW*FvMb{pr|FBTN5yFCW)#UqG%#Q8j7#*4Yzk`7+LZ#);145{u1eHf<-&l8;+la-LnbaYy@ysj#xf=S)@9yq)d)Ng<)*wO-O0bn{*U(BMhADbg#LmW zBsn1R2WEnX0Q@_N^@}R)?>ac^pFWD)Uy1amHWSiEqGB0?3H@xF5)n|+C#~q7W%)Fj z5m}hP%@KxWGK;1A|0H|;xN`V!v$my5r&iyPD`q9(Y_xtUc1haC-@4~T(fq-k6L?l1 zmvapS(F9>Ta99F>DJDVaHM`tLbNo^X5d;8N&bsSu`M zDNv#z8i}E*2!xUNlt7_k5MofI7HSb`Rw$tf8bM-ELV$hW ztmBA(QHc^4upAxM1q0Ok1pVn|5Tu!cN~(yeiixHwii(C|DoH4bf(U{NVvweq5Qw57 zBhi=@G$9fMRYH(SK@mtL0F@grua2C?%F6gn%eW zsc0pFkSHr9TtKM-#EMrW8dQ~rA{GpWerI}OAoO>V2XahDez@QU5rZJJ*uc@NzSU%C zHXOIiN@0{d1XrcNLjywN_M!)w5Is*9kbM#w0dlOAFsCXEWICTbRKfOr0l<=-dMEqs!Cn z19$?7g#d|Fjqp71$WXQvYCx=q5ZA^}6#S>Mkn%&^;yc0z&wMor`8{SLef1;~lnsA<;K(^;a#Xq67DDuz-iY1F7NK?f*OxDlc4jDkg) zX;_iOB@|^?J^N>4PT?cSAi|jh^9K?RQ*65;aR_mUV0Z^g7zxmSJ@Y-xA>1!60%QY? zxxdlN=)gxh?QmMUS$LvQor!_(x%4F`j0rbGzPLT~OBR)r48|@nZ7Ttk%$Tw&0!1)m z`OoJVa(fOpq!b{DSpw+bo>11ule{Yk?}E>l!wk4w4uleY5Kdg>&yXtzSa8&$gITn( zMI(z7lT9`eAPg`D?G40kBGoY7Lj^&Bw_Q`nCG3t2v>>r)D2&Xx3NVu&S~&<5M&$%A zW!gExaILJ()H1y>RJD%@MmFB{zLgu`X>(M#B6oJZ`X5CVJKu@|&KW8uk!njyrrBhq zj<3nG8{0wffuW?Dq6!F;b65_|1;Dj#MzL~Wf@zox#Gspq{B^UTO!CQft`kxyU~b$k zR@91(Y9npqA>G02U@H=x>cL)Y?9VOi4VWpr(TIkP?AF#7ViAbNL2{6ba+ImIp>y@( zlu#D8R52@zaRN2obI*udDZohS+ZSjAR!r?kY-t4v$uCh|mF%!xyXF9_V;pRp5jYu( z=X-n0;BfB@@U%pf;~qrt2uuR!Z*L}C0zyd+froK0p&Gd??VL>Tb)!bSx7?DX1pRGkaBSh4VlIv52!=tf#ne=H?DPl$@h;;vt}eMwh&ZpH3=x1ZOwyXp5Ney9)+)8+^{pBfREwyMHIu?(dh+ z{Di|$&_Y8H;D@i+Q|}kB`*f@CJ9YDAQDFlh4Oh^TpfPjF75|s#%h)_;P@osUA!*ElD5^;>TmdhKb zA8>7e&9u11DXGbcbv2T8*yCZALqJm<GSF7_5|GmQ+O89 zzw7%*79vuk1Q0>{zrN=6vgt5wb57M$UeAt)^sycD;iFm%)KP_$h^b>l8)`}Jr6DQ^ zl0-x=1rC$Me!qbje?7Q}f}hw4@jnjq@b~jJ_F#G@Gi$l?2;gbf&v=uvnZwlmJ5y-jz=5q=y`!C04T`#iDNed_TTxHWfp z`=(KuT0bpsDGRmUJz(2@AB!YPtfaAawI!uQUdYcm=I)o}b8-e{Yl^ZT2RQ3W>zt9i zo6x#gi?<1#c3HkMvcGB+To_@#12)YaHX52^^VOa_fj$f8A8f%r6T&qJeQ|+C#gNia zFi9CM5NL%QLO>sw%l@$gqihYV2}k689E-5`Xb)ohQ5*SLkO>J49-R49EDm{?VSx6_ z*doOT`1`}hL?=(*zVlEf3KEQg1G*J7OClN$(9)U~UOaG7Hxs!PB}I3A!}LSh#Qp$-z=h~GJ;ycT{h&ex z1tM2i{aEL-j}F=1)9IR|O*yJbJXZ>{8MOIt$7f8-i&?rcn}XW&Zc0`O&VF7ELtx!$Yf(QY{G4VG?mWd67`}_T`r_=A**0XFA*&1-gVi)b$ zzT)3=L49w5o#8*ma%yAH-P@ z;mlsCD5M~mf{G{zU{kN20j5fZA_$5If`X9>27nqylm>*LTdac+kbt6C86hbeZLu7Y zq-CXs7(g!6eW!qEKF79!+t-=$^I*nF;~F$YfK?I&l9XiZK=y1N$=b!IWH9XL@sMZd z2hfrcrXV_7D@h`Fmje+r!VsB#Vr~@K=(;oaJ_>%2l$L;qlwvy09-#B8$1t!-7=Hh- zVaYpnG^1dmIBv}hnKhFz=t`{(pTqc8W6ay6imRoUbr>SVg+k^XK7F(Ychjecxuuh#-KlwX$6{5SdDhil7LqIiwC5fQ>OSZy5A&%|z~CE-5qxCQcP2 zRHV}zjA2V!%%g@YlJH@vmqS5VZK^2*iIIpcA;HxQ6I)g@^TgSMPQ*(jL{L=|Q9&4G zg5p3BK`n%|VPqN2#=$5uiV&(;sF|dLoMLPi5;UMg5m3xQ0K^MR0W~nv6b({TQzI-C z6ePiFa@q7l#kpIM6n)=j8~ zcL9$$y*95}yCk6;j9|BwM4=NP6qv~uE;Uq$Hq;ehAUG(3!Dhvkh81A4mH|aG6}ZWo zrYb55#6@F_X<}IpH_grSgN?WY5*wz00_H;vRgG4Yf-=ZaKxqtM%4(RvB{&<=J2{!TJmdBD)C98 zBE(R{g0&z9XPeQyAaKNnEl0MufM)dAnRcxe0h2Baj3+_u(;3wJ--kS1up*jk49^ph zFI5~3`^-h;!aeN7CJ>fc}1cBHRdRg0$;}D-cj0DkPKww!oC6dh?v@-x?L_{Sb zbV%%y7-_;ZP6_e;-W#HH^$ zt+#Ci-1_Dj_JbydA@yrNhx2}h(`7g0!w;KzN78a6wJ-c4x{Jb2BbTVcngbDxb!9CN zHuTd3WkjEXs!<4299vJ zjY1UL#HmwCtZE~K4*nwkna6%Ok|^3;&Au~qYSXc_o_R57gk5vHufA|TAt^g^@6Pjc zCrxfm&IJ9A2skTnabe)(K?6}|At2a?BWNc(hH|&*r`_gVsqHs3!?GrhGt3{ghp!+ z!eRs>1c@~vLNqIqVuAJ!?xC_s8t0$nH;KF(vEk|V^7W@iK;R2;0`yG=X&zZfCMf1TgVElv(fBMb4T z1rpAq#HC~Dl0p2EhjN~FMuLP21R?>zFFc@;WR#f*U`a}WLWW4tp_1WJ5=97s3;>w| zNU{zB8GY}T%Bea9&qBVT{;$~j-VC6eSXJS`;wT4Khkk_(Lr8}E(Jase&PNt!c6G>) zXLz21<^pOo=OUn0s%jk5pptwO`lcmUVNgF=-Na8=Ul;m3!+H%r6MAg_is>{nIf!HA z?#;~wiGYxGu$Whn-q@An#6pE!Dy^Ct6&j=lQpD4}lKVcZ_GO0Zpsi*7Jm_fSVv9Lz zvTTw+#)AKDPI{c(eb(_9{8qJob$ymz8>!7#T%V%etZH1d9jWI73d8z%GAt1m5V^8s zh#l1Lx?ki*BAiniHBm2FIB2`gS6thkr!bx1WcAbF5c@=gq}bk~BPl@Wb2=Lxq>BFZ z532j6nIM*cp`kr84{?`-(L$LKdeOrn@6bH`KNb6w{DI+8-*nK-3nv;) zFwDm=4yH4Quo=0gma+KZS2l$D{7zctmOz}6ewK8@>%xAIvXgC~2ks+L4uwZ%Z{!PZ z&ASbL4R8sQ2M@3@zzH`X#zHWJ{qSHX`{K*~ulf@P2&csHuY7p6urMPB2m?Wwhu`;_ zKLcRqb5;Uq9(}1FGFnvB<~-v?NhY~dwY`*BM@+5m~k){6grhZi@{Mo?zm6m>+`|- zUr(6P_(=fyFo2{B@@LIG?Lfp_eMP+fm(_Y#SjcUqV7z7}{l;dl?7OtT=g`YMPXXuH&p!cLGJ?r$+6A^ryx_T17NScDQ8W8O7EK zvs5Gl4eF=&c&n+mE?@(3X>dW*-fH6KefJM?mj=g!Loq!6tr^+v(SjcNl14S%eySwN1gbm z%Ag`z7=WZ=0i)6EC_Yv!&~YKi%j7sFCK#9N+eFl6g2_1g4Z{;_31|UobB#8+~fjfor zeBmdOBJTPT5&I+s`UI9?3_*YoBsoCDvZiJklQVD_I~_;lb>-pLq#a7kw2KHsqC=dz zEDJKTjjo+CMUYrQ1W2_HlV$*{YgUy2t&EGsAzAsEMFnA;E*`lLxA&Czgc?e5ocLty z5SPA_=^#8AKTolK=s-U_q5;+pn*ww(t4N=aJuW}E6S(f|W>k`22s?NOOrP|>j4qCl zkD>U7yhrT$B7C01wmWR~zajp{2&hPss8OI)qLE1=0!3(v1^q{rLV5x7x6b7veJ~mx zZ&3Acy(Z4Jb@I{h&uV?OD?>p3ABP^URu+GIXy`u;Y|dvoIUBbfVKMp>!`t*P!dOZS z3Q`e=i0VKy&+{Umi0!Y^Ha1lk)@1`;$01~xMRhB{i^C?3AJ!R>uh&NEgybU}mGgIf z@L9Sor0`S_gDF;8F&L`BG#&M)0GecXHB?J%-(v!73kfjRhp)vORqs|uP($M`?6ptL zu(Ia^cZ-!ou_S&-+uifPtrw|-SWvN?#_IdlvbFq{QxJKC{2ij{$XvT{HtTm8)ibua znIIPv^7!JiqP83+hoY;U=#TB6Erxl%OJ{VjUPL&3ei9`kC>3y5K&sc(V9w2gq=362 zt!J?fj+7?mNFZ%AL@=Ri6cWLQJ%#}2-1-}d`!jh#EP;(6po9GWWe7>;{vS7)FY{4Z zV$z7JdNi!E{#Jalj<#9>0w`?Q#@bke#jN~@79DXIKjAR_?x!2JLa^mi1f$M&^r`ue z8}^jjWv%kcBBKD?S#egw*dd=t=ZJ7JE4iw;(X_H%T07c|)7T9$Of~DuaF*uAmr9we z$d*l6X@SfsCc`$#Lou*bu{bJKY(+dhKU~c6CL|!xF&A655GUvfOizYqTdBs-<$4EG zhF;Bsf#6*vN+D3%DovWseyzU)Oc7S4TGS459}^*^^WDD^vvsbA1S*^|Qc$@^!E)U- zf$C`X)nR6c(}k$&I@6_i9mqaFd=NjJiU*&#=dZ;2VwL)($v=`!FzQyDSf()Hn1ujB zKvEijas@HKaSy!dezr)4LKo^&fN%C9JOBXfkr*VRijjMe{&~S&os}fyJOmAcw@yv! z8NRQjybfnlr9p$NY{>93bhw=v2BQZwc<%J|23nij?ddpe(s8d9L!>=O&uaM+=~0C1 zl?fn1W%G&I%hS$)ssO4MAzzs8ARF)5_xZa)HtpNmbtr?^0ZOAyM2ACxfsO*;oIJn< zdV0>@_%%PdUaCxNGoM#3UwH|~@fp8pm|+MKN&pCvsDv6}pGh@ILnA7%`pNK6Irc$E zUs`p$Yi+ zvXv~Y{4wSm72>@*yFTK^ueAdZkbxjaN8@j@TqPZ^;g7~vhcEEq9rM#~f$5F+*LtlQ z(}0N~DoFxHHsF^?eXZj-=kjX*MQ|g@)#L~1=??7j-}jO{jW11Cqz=UkJF7XvYZj=O z%iP0qe%`Co^zesjr69Vie1z%YY*6bu$9MHCVRkYHgp+FB%% z3978sQy=6u3{5IYiH0J?5TuZ@PSZNOLL1GB!GvQH$^}phL?R@Ru&N9z5I0PbFqN$= z3ef>^BqGRW2+@I>6)Z`RX@DTfnjxXg0{~eG2!%{kM2ZRmz(m|BKB-VhCpcDe&avPFXKP505ub^@erg=84fN)`ghqL5G`z(feU+}nC) zI&xXGz*1$g0D^>$SptYfAp;OD6saKlb)aP>7`Qsf*X;)&x`xbN#(3CXA(iI_U+}|p zVQNsDrzKSwT685u9Z4}TnQEfqh-GNzM=4vBZB`QXxHA$BD?>1bQkbl@YUwa47D%p} zM@>aDGYC^KLm<- z2rYo9m7$WB+C@bsinfJ@wl0Nsp;DM8V1>BWK_2>ncIz#OS#X1-lwwUS>PktW>5xoM zLspo&8!E}8Fsqo5!NUgJz_$p6NetncHK!qqG6+{G*C$=5 zS3$Kwj25&WWyye%O{6X>HGzh$V45tUfJ9)B7+D(8u&>|s`)1n|F32*8V)bZoIOM@1 zi5W9%P{UTnM5K_2F;))X-O4GFwkI~gFk&5A0j9DnG?*(g(P6q~f{=!pFhhzNm37BUuFr$C3p z;o*kKy(|to;hK@MCuO9GLKxqo9ay=>wRf0a6`!hT!mvU}ZfLS0p##S9Ed(R}b=5Moe|jKpRoNSU1u) z=RnB~4Kvs2?DAh+(2~pBK0UyG6Zb}H*E*1d+Ee0W*FK+n=6O8;e83;Xfk)z}##SK#DGF#s+CW~^Og03-@^E^(y}u4ZF;p=p$k zD2``szAxw|5pW`E#Q zWactBI{;(>Fo!oE)6}qZp5w1tBLiph<6vO~7|i;RJXAVfJ%}&J{xO|10<#^&_6o2Z z5=jQXCIN}~ocHkjFOHs8o~~Gna>hAe41)migrAqZ?m8T^PM8pz!uvZH<#XXo{5Obl zrw<)Tkq1mQx>Jwl1vna+p8lTmBMHGVrh$k^1`|Qu9|;el90bsd+L!Zq(~Sr*6bipf znB9-5&z^1HaY+v_Y4eA{4nZ5vDZu$LA#Ye*<~_iTCMFg?EB-fw!R zge3&BMF9aMA^HIxXo1*sIs@$LQ4~IxVdc(<0hc0Z-I66J2<(2(0ebuI=}1VbfeL7h z!yw(X10Xs^no>v!B}pX;ScVuPRRWM25CW8F5~8Alk*Q?_sU4VE3fP1%x62`;P{Rx< zp)^A&Q6*V&l`=86D%d$e5Ls+B2tzQ#Mrvs-%+ai)ts|AND-i_Db*#2B$Zs+Z42W*V zDmqS&SGCxFzd&6B-Q&xk(GYthexX2lsSL8jsKF?^$r50}gp*Z7ArcI*U<8gt!AM4f ztU&gXgiyzE(DWI5K<=9DI!I0(2u@+s1c;SsV5$XJghT|nG|3d?;!zPz1q{_QQDkJZ zRKz5)S)RjAF${#zQWPyBQlUtqQYA53j3CHJlL-kYsYoTopYg7pwg|@=h6WA5;2*`+ z?APbNAE#{heGyH%xZsXj6_m6_Jp3|ZdjB!~-nt(ylaE{Sdv89@k7>M4obw?2sq5dQ zF&f}K@P{Z&Y2$=7=1JV%E>E5zf84hAwj z+`D0aZ*!QAIX+PSZis$JFpQ9!L_*4rO4P%TLO*l~h*I%13rc6(JafGUdYU((bQ$7S7i%3YU)Ud^D+nvVK>tc~PW7$aboC(GZtO zNEAy`zC(qS=Rt07QjnreMw3km>9g#j=9HL93TX|~LBm=++d`v{SJt^%HChqQ^)C)h zUcD)G!?nW-WxL$(F8vk3T||{tRw{y?NU%hU3=(DAC1BjcYiF=d-qBXn{@h(vf$rco zshOK;Fd}-Q!jwia^vaA~mM84>g{EO)He9aegCxw5)tVrzBb5rO%%?4UwS}~juSxc9 z@g1sX4CvX)%NuPR1D$og*Xh{ca>|?%l}fQhWu?Q9lQY|#JvUvIJJ8pc@)bP;D?zEk zu=nZIT2Ahy$_XVF!?zkS^|#G}-U-_$J^OZen*;Yqt@26C9v5tpSN3FRPO1_TLvC?) zvSLb?gxVXBGJJEuj?jD6Yj4@tGf}n2hi1Hd`?9^-7`!vgb?@#{6$RU>Ha}raI%|e? zt0VH!oUr_v($>;v3$@o=#QB`R=Fh<{_rC34cH*GUqg`?7N)w)2OCe=iK5}?xs>LyD z2*w>nc-vZh?~A`)?iiN6(nZJo6Ovii*!6FDZ^l0D{PtvIwJHdUkoCf+TGu>!^loV; zydOz7M}tXQhViGa_LguYVRYJUo8GS~!`d8YXm?JC3wvry5@*7-yLq8CU2iiDL1~GX zs9MMIj;~THbtD~;wI1oIP}57r6qD}onpx$XHS#}1>ez1ewX2*{Q+3oJvSZOb!O&f9 zm}6qooQ%mjy$4A>c2wREBQY7m9W~gy95ZC$P*#-^wl!nL6TN3O?lndHTCI0i=^3wo z!rIK3wd)~mGU;UGmWhX9SSD#H>uR&KiM83gu*IuHvfCRzL3YuPuZ$e^B>S>8& z;V}NCUv|R6sQCzMehz?z6?H1Qb{19|aU4QLY(ss;!co{2cML(>9XqzS4H2Y8!dJ>u z4hF=UMj-=32t&bm0ijc5+d-pb7DWb{24;|gnmZ82=#f318pB!Hr%@djJ{6+O7e6}_ zkfbKOiZO|bg&V8C-+C}W?a^;WTkdjIPY8<<7%c=31CgUWU3Vnq<0&#M0x<(*FpID| z3`v`Y;QI+6Li?&V!cj2JIbrcQ4TZtxn|wTt2dlf#Wo3ZWzs zATn%+7(rP9aU+b$jM~#)dD;mXw2L-Sb0?of^wj3;&07>_YQCn@=33TFDge#bp7R1} zD8|X%$A_y9P+-ACz`^$!HbNOLlzs45bvf335p#I2e)Ch)Ul>ws1feokBaKtEanrPx zha>lImED=g9{zC|Og;O_b;4RDKF^kOK6G|sC0x10(1pFNDI!`+GLyzE%?gio7;(kk zyH%RJ-0Rz?FT4;+>>+YeB$hn24j@AB6gbmS=_@%c~yq)%KLuKx5oE<#fBp7m0{8;qpoB% zxcT^uM78W$ahhE(Mmwszm*)maq}Iwtq^O&nOT`3myrD8UA3+66@FA$u(Kf_7&t*(e z^vwGUcE^^I%1@%;G+fF>iAm(vnC$MBbbV7rcr(E>drOl~rvg(W7^zZjaoP?OmYZ}O zmsPqnei}YgVlOCUNuTH3b6LCaIdLbYF@}KW?{j)g;?`?Tyx=R!&}q=eS2$7^N!7Rs zq~eP?UXK4?zUNV0AcbY`%3LA=%;F90l*_@lWF(9?xx)GEIOlW-u15 zxf(Mt)dsPfCc8^v-R(MDP1AwEIGYCOZV+~kgvNGUHel1cBh-7j5X2NgMHLnN__$P2 z7L@vU1NP{Xp%WjLoUR7O>-D~e|9xKf(dPQz*Qeh2{q8>B zcja}v-NJV%;?hcb&^x`-($PJSMVt+enew}tc#az3n)Nk#Yv*f!@cVPP$czrTdD2(B(tzA@=HBQJL&Q_B<5>d2k&!R>AKs+-)ui~tzT#Kn7J*O<{#6d z(TIPJX#{9cvd1!e*{rPC*`o>t)&3a4wu-TrG6O0Nq-bpF?_l*t&AVZ7_S^Aa?86Dr%m9k+*Zdc zk{WfXXk3#@W2kD)I2w*-riX%fs?gF>e9J7eW}cQaElSv3I-?6)$7>TXgdpmYvqmGH z59@%Nj`>{dT%4qfQ7a5%r-2K`=M70J)uQ#1wPB)`O34SKT1MLv8yTjOFx9B)|ElbKccp^7&h*MG7S9FqYn`7R!r$i;AROqb}M^`f`i6CP*Zo^nu z#dZy+4K|EqSj1Rb9LzQ~T{hXHYcSBZniR8i-fYLk&L*|J7E+t=b*RN=fti*r9nwi? ztpujQiHt!828jrgNlfJ}%d}`hRd$VRLd;S_0a|xedwR;et0(Nm;4#UFRdtXmP92w%L?)VO z#D#^uJ3$2Lyvp7M)C^I|!IqMPdydCeLDB?b3re9{hSmnWE*t}9a1NN7W{vfjv5_^a zrzk{@Vr92Z$0{gP(3@%lm7GgeNg7K+W|I-EGe)Z!DrGv6QhLc&V=@JqN~O_s)WYnk zFsv!%Vl%weQo`}Z=uBxOS{oBajZCGK)g@gcd)bpZE*NccV4Rf8RLx!;r4F}!X&WZp z=xGcQMIxgmW5!#ndLtbVWxK*`4iL1En1WQ@$DxU_3JwkY9BjV)1-Ms;ZY2d~^Kc-gSy zeOVB&r|=<%(i4qGfzY3B5+0p9zaKthN*=dT_q{73PR!)%+gN%8b%40M%^)%Q=KoF|9g%%cr#5~EQXY1224Brzssd4t4y3>i=&o#ah@SNqdJ_m z;;+EMpZLv+jl4O22G)#3nlaY|pesXng?yTIzA4!>&Tvp*ueaQ=>T*xv!`=HVrh2-yL|^WZeF%1PxVcTAXsTBi zL4)YxQ?(b_(xLx6*{%pPRF5?Fy}R!tPF1|o{qvyU2cHhubX zDv2%}Kib2accJL3O1)b+TRHtP+usyk(n!S4W(qS+BG_BuG6?U?_yP4mfkMAxc+mu?L(R2*%RzwT1&I$aOV9 z%k1uG(;&x!lsu9sV8W0XN@;MyS;7v~oDGMe=?)Gi5Ya%0%#50vmSHp=N0*pV)5AR7 z4ET#d8cJy9N7h)NPl(Jcm|;o7f+Wnt6EU+41z<(jpI7!aS#7y7gX+UN1%nJb24X$7 z^lNwH(u^d*Y&(|C`|_80u1A6AjU4obAEs9mQiS+?&!#5 z=P&3Aiz^)n)Smg<-Iz%r1EU?x&D0yNMvbY%mmeX6aM)=A`Wp_-Cs zMl*pVFSm1mj#yK(AX)Fu0z}a21S!Ll7~DoBfCAKDVb{9W+EF_9>v_+>=3&A}_Pe3u z2ov0F6Vg1JC%*7AURKERMgiZoNO+thMNHTZX)ha$6qKFyAJ+}olglwJ1OI$mZ> z%T}wob#C{nHY&WvnW-?*uh&Op@$LD45d421_CIUa%_Jc9#6lF3O&=)l_R%TnDc9pi zPfT0Vw8bYo4kj(%1~N_aG+b{JeCb0X5$}x6GrdK#T|MkqOfua*?9c6PZ$fSowO6CZ zj+U8R9~6`6r%pl7W1D0?JIzj-<(Ef3aAqL0Mcb_CO)3wM9qiByNTIAOAqYbNs;rQR z6oeWOlmO-sFqF#47zhvW;bY(k8(YQe*Fn1|A7ctCTS^MUm0_EbjyCn9aE@?q2bU#z0IS?wnJWD!#7=&T2&&dIV9z?Mh7aWvp5M*;Vkr; zYf2tdj6#PgHoF6AhSb%W(nDFVm4Rxixf-Du=9-1rNqSfSU{XR8+>lPGRapE=;Re^D z3{+Jbw_325Wp3%6uxJXw+Nc16r-6-f_HfNrBio|`jdZgJ>61{-B5Ogx+&uUZqs!U< zWME5_;MGV;5I-h(mYU6|`x@zd@3j>ig^QGfvFA-~*xB9I#xqWtv9xxw7GSJISfSgk z^DkGU%5%L(SqvGQWpt=w8g`wW*$y{aN~${CRB%$Di^Xpp&wX;l95CS&5ttlKdFNfX zi($zrd8AswdUWDt8OB&m9gjtqj-c(S*8?pM*%euG7^u+Xb}|}V!>)HmmDZ^Xj-b>+ zrA8w5@0{V?S}9jJ`;Vxs(=%Q7!`C{5y^fZVh~Vn`n7S)`5}}s$QRXfaF2Q3iilb)i zyN%r}P0+$~tlVTYX7Br(O|mtqq%H(Av7IQDE?aG{y$^fIsa|w67s^i*=}1nG8QHFm zc+BlfFRo|Vd8E~LwKX@(<#4!daQ!)YcC)!L?dOa#K3_`weVp~TxxLq1uPkfQ?)%BB zHl6jBJDI#Pqpd?-TQu-VAzGs=%|AS|T#butVY3+1n8)mz-mg6+ zN+%gC*@%(_PYk&{%PM9llb#DBezUntZoNFU$-W&{z2|(xj`v;VDuUXBdJHC_x|~mKMs=P024X z+7niY>|8zGT0IA+e=b>=goK^hI5_UYHbtWv^N4#DXU%GR&&bMhTYn z+%{s|)Fz;vf9Zeku~fU21umpWNqlzY7d0&Trl{UcTLdBQLcU0A-ET_6ZHtH8A3QpVC~n;d z>vwt2W_4I9Gp>rV-6cW+1$2qbogK1OBq0SOFBGgr`WjTXrtJA?#Ca0Z>#hY}6=Dt7 z2;zf8-n=4ajo-U^qf3f?I)7tmx?-_FZ-G!N2gcm4uR5&CCmxnW2MyO*vJHc-)plC( z%-3eCv$N3R%~zwPUQSfxzHu7#WZi9hvIA9|apqW)UKbr^*w01mve>hz^OU8*Jn6w0 znNx8$Mj)#w#!4GIosptAD+6Gd9F#$aiDfu2H!$4zZw@Ac?S!Oo6vxC4^yX|y#CVIG zY6-%EB6*rzUDsik4ToUZIUT{e90b^`!qOdUk+C>gMPZTYQSwAVNh=R(7!Wlf%GEnC z9MM({U9Q3wxkmz%yQHEgBjAd*kf!q{K}AuuY}iaV+p$(abd!vM<--v=)&d3+9w2gJ z;enV_fwVdl)ZS2_$ajW7*vU0JLxTbbpPmNGuyIy6CN7;XMA)^HkdY?glCiRfFc3X6 zG>?XWrSwOaRX>>WTD8+ER($PDjSa!m?XmJG;jdX#5#wA#7&ub ztWi|6lTyJEWZdAh0hS#n8wP-PWKA+J0cAJ|K_jy>!QJ7^*qrdGs>Lk4LQH~=gKkw* zKynQ00OIZ<2U;ucF_-1d6zxOG4r?D8V_gf$;HmGbVG2PL$B(?v!=JhH`ol&BSlLR#@2^c{jka+Q$Q&&Y_wGC@*4aOKG5HHJn8+ahq3-f4UV!BiES5fRs8;Obnd$D;iA|FQ>|K*Un&Ur|6=Z!+dkbw%CX&28~Wy2^N z3T=8d&!3>d4K|UfpA?g%X9^)_60VUgG~UBG#cAOkA_?iK5|H!&Lca(ZP=#Q9Xm+PC z<;>}wy0d$y=g-`%FIP|_Lec?G%=Vk1RJcgiW(G7HHyKfr5=c?Gk#Pk8TWY|TTBDG; zl7gz?e6$*l4PjD)HA)F7hC>2^uogsNF~rhn79`X;h6N+GXwYO7lsHOau#})n5RAE{ z)PR)O21sjPKJ=>m=iL)+psNMjS|+w8)bx`eTR{$SMVJFmca0Zyc=|P&HB<2Y( z(t>%Qo!!w11F`5g){h*;AI#D5**goetlc+&h$XNF$QNM5#R9vsqB0=_mSr5cWsGn@ z?X(05vpi15NKaYPla%zLAv~p^!5c~*W6QsgdYVe#6!rom>9pOpYcf*yc_x`L4AKg% zts5FTh|(3iqC>V#ZD8UDy@d$pMDdzVk^;@EC117njiL>)odS0keK zXA~Fe;`wMWjF7?=9Z1juG}A~@DM^fjkcr1ZDfm=@xC3kjvgl9D8V^BkkC+{W_ifkX zz<$XJ0Hsek^g|OEfb|c8`?>)?Vh^2x8DSBnPgmhB9gE_4K<&o!uNFFC$HYAfIWikq zI}MZHCqwUd?eHkUfl7XUL&q_jVYc@3fZYr`4X`&Nh)9}9BYeQbjDlLGV8asth(<(I zMp{@D*eifsK(R$nDKHWhAchu{A|la=g&2V@5REL8Nk~zdkuVZdQb9n$G{{R%1xN-& zQ6j(?43IeL{yW>eL(K6xk7Nn&>sh+Z2D!ulCSPQl65o=+tnS+}8g>;yjDISRQsKhIrxEOkVCzz;%BC3b9TUf$WveOK6EV5X}913WP zA(Ex!BKlR@s*xz5rKy6HC`TtDLog>F&)eVb?(gglu7jaEf$hbLVqqu+k}5j?y)QvU?)8v*mo};` zr%Oz{WO=uu-m_RC7Tz|IY{F3pQ&iJwI*=ofZfQ;L4cwcnZuF$n30Q}DEzQQJ7G+i@ ziBEE7vnsT&3W%yyiq-E-vqCl^uDbp$4*=+12Hic}_Cuf^2%+)e@r{I2{?ft{sL4ZA zT?ZqP@b+_gQOQ&{1yo^Pr0G{n5X8i!$+O?CW=C=L8>jb=efi_v-A=54vWx33FyDLy zFv(bV2A!sWpx24za@%-#+2M$3FSZ@ne>nZWMvj9&I4{zB1I(+`KQP1!L1vN)0bu&E zI#7$pCzLQpXv8f2A}Au=dQh%*7g1|&lb1_Cgj{)ITA z{{aAr(L+E)uyl@v9p(P$q$o(Cs98X&pbAMuC4!0|A|#rsYAA>z8IY7IYD%afh-rzW zs6rH=3IBiSL`|8hvlAV(!S*XC5U6cn({ zO-)P`5`|GK0aF7&LX5&MG(WheaRXFHr42w&2U0KUwmjk--}At9kNV7nuz34D{fd97 zB?w>&C?%ReckEB4_s;v2NdOd!NkI~k;-mkgerEwWKNsZ8{I`1R%rDZKg6Tx=UtlMg z|9G1kHmAG01`;YXrBOgs4x$)wgjFCu>?a1eHmFN!5?q1)=y4!>*&x7X@b&zqCTQmQ z)OytSgD5Ip3EdP#0YI{F3`0I{og#p{C;U(m2yc)WqA=L7MxuhzsCzu;$<`N|Yh8z= zzoOUMob_J^8(KLpSN;@-n10~;Q2D}tY*4fPD1iauAKZD0prn!yygv9AOk-t18bnwP zs?yO51I9Hr4+)Z3mL$SSRA+V<19x>h@OBXF;+jtYHbF2SioTdn!hgOVa55+#I$TfH zK-q^W@n8bLSZqDhF?i`UCZIv8m_Nf0IpGNKg$|Ae05=|51YPP>_DCF0+k$@I*S`&v z5c*g{iTEZ9+8ulokL#>jHv;&ZH!!d_qz09b04+0%xLs0zl!&fBtrWHDNGo;YO9)Z6hrDB%GKY(PL zWHjfW7O`1rMT!bzZSrvhk(n%%V#1L{KKF-Co?rQESl4&OqKSFyrre{UC=0Qw8=?yn z>cB$}~1C0$nd!OS<=vq#9leO**Z6%Vw336%=ZaXdYg%Maa|MNL^X zFrGROY4`rmqwA?g*Wl(IN@Fl(j5U-oXj%#=#A#ewR#H$?A(Jx;1TlP(gd15#K{RLr z)6coN^MfE8kn{BXw;mHK4$0wVHa%T+wyCirbEpS09O9M`4*DBW6jojp8Ma5LZ>_SE z;u$7%8v)pPZvARXtlLgVGz&^lqe=2?ErT#qV+@s+;*%Lt7`?;8)nTXsp*W<61h|9} z65|2gqrq?vfZ`+5r46~)mXOmbXf$e_j5}di2*f=wI?l6{?I6VMqcChHXc);$igt{j zS2+FniuF97fS`wXa+7nHE;#!)*Zdw}@Zc`bW^+Tidy;vdX$j8l;s4I_G$ zF5RAmM^KbN^m?yY2Ld-mcmdH{R z`r)I0W@<7A)g95e9ZWpUawh28*G|TEuM`OgG-y#1{wk7Sg{YuLl%N(Mq@aiI-DX6F34{)ikyJhZgdy}OADspF_TE{amzE_Ow+_rd zIjncJbViTTXBU67gZtONC}Z2%R#^jmb9ivsaZPg1xj2kI&$|QXb3oc@gV((|l2IV) zAsZ78j-nY7>0`Jw44`U}p}pE&i&%GNzz7!17hiyl!l=-2nQL;NT70o3c_OuRuBq+c1*)_IQ{J9 z99Jx?O#v)*{D`mRtm3A&ftO>WNDZO^|2$`ZO7)yT1Fr_~hmoMnm|;>22T3HGhT|CE z8gw!y9ggDe?sIR&^EAe5jAt|9&L{w~%sCnTqX{z~y3C3MkH|0)1Qe7}EXc7fEEK@Y z2*8BWp(h?7Z`R89hA8C%gh;HoVoL`nm$%i$lg$I@c%FnGygs4kba3EZV7gsQ z%zr@nHw6qoxBZ$-hLQ>ZDFfJoQjqr95AperEc7S9dLQfH9uJ4E*XeT~7r1b~Z%wq| zQZy&`*&(G=GoU{*A;(MpC$r)2gCIr+MKut?`EvZeQ`$fw|KjdQrwS4jOPNYQUOIW- literal 0 HcmV?d00001 diff --git a/src/FAQ b/src/FAQ new file mode 100644 index 0000000..5cefe5c --- /dev/null +++ b/src/FAQ @@ -0,0 +1,13 @@ +================================================================================ +FAQ +================================================================================ +This FAQ regards the source code and its layout. + + +Q: Why reimplement code in the part rather than consolidate it with the app? +A: Mainly to make the application start faster, loading a part is not free. But + also to make the part start faster as it has much lower requirements. + I admit the maintainance consequences aren't pretty, but the part has very + little code, so theoretically it should be easy to keep bug fixes in sync. + +=========1=========2=========3=========4=========5=========6=========7=========8 \ No newline at end of file diff --git a/src/SConscript b/src/SConscript new file mode 100644 index 0000000..31c1933 --- /dev/null +++ b/src/SConscript @@ -0,0 +1,21 @@ + +############################ +## load the config + +## Use the environment and the tools set in the top-level +## SConstruct file (set with 'Export') - this is very important + +Import( '*' ) +myenv=env.Copy() + +myenv.SConscript( dirs = Split( "app part") ) + +KDEinstall( env['KDEDATA']+'/codeine', '../misc/codeineui.rc', myenv ) +KDEinstall( env['KDEDATA']+'/konqueror/servicemenus', '../misc/codeine_play_dvd.desktop', myenv ) +KDEinstall( env['KDESERV'], '../misc/codeine_part.desktop', myenv ) +KDEinstall( env['KDEXDG'], '../misc/codeine.desktop', myenv ) + +for size in ['16', '22', '32', '48', '64', '128']: + KDEinstallas( env['KDEICONS']+'/crystalsvg/'+size+'x'+size+'/apps/codeine.png', '../misc/cr'+size+'-app-codeine.png', myenv ) + +#print env['KDECXXFLAGS'] \ No newline at end of file diff --git a/src/TODO b/src/TODO new file mode 100644 index 0000000..a8d427d --- /dev/null +++ b/src/TODO @@ -0,0 +1,38 @@ +1.0.1 + Volume state saving + +1.0.x + Improve error messages and error handling + Consolidate error handling code in part and application so we don't diverge + No audio icon in analyzer when no audio + Playback finished message (amaroK statusbar code?) + Mouse move to show toolbar in fullscreen mode + People are likely to use "F" keyboard shortcut or escape after film ends in fullscreen mode + but the popup of the menu prevents this, as for some reason Qt stops handling keyboard + shortcuts. We need a solution. The escape issue makes the popup menu less useful. So + escape with the menu should exit fullscreen too. All shortcuts in the menu should work. + Shortcuts not in the menu shouldn't work as this is a modal menu! Could be dangerous. + Consider adding quit to the fullscreen context menu always, or make the menubar show with + toolbar when mouse moves. + Add tooltips to PlayMedia dialog for recent file listview entries (directory info etc.) + Show zoom percentage in statusbar/OSD when resizing window, snap to 100%? + More feedback at playback end + Better loading/buffering status/feedback + Save prettyTitle with url and show that in recent file list, + - save m3u's? <-- needs thought + - mark remote files? + Support .srt files like Kaffeine/xine-ui does, ask berkus if stuck. + DVD fullscreen, DVD menu shows or something + +1.1 + Volume button (not there by default) + Two entries in Konqueror: 1. open in new window 2. open in current window + Play from begin scroll up popup when resuming playback + Much better DVD support + Show length information in normal window + 'o' in fullscreen mode shows OSD of length and elapsed time info, <-- emulate mplayer + +ACTION + xine config dialog is modal +REACTION + none, at least yet, it is far easier to maintain modal dialogs \ No newline at end of file diff --git a/src/app/SConscript b/src/app/SConscript new file mode 100644 index 0000000..3c35c32 --- /dev/null +++ b/src/app/SConscript @@ -0,0 +1,59 @@ + +############################ +## load the config + +## Use the environment and the tools set in the top-level +## SConstruct file (set with 'Export') - this is very important + +Import( '*' ) +myenv=env.Copy() + +############################# +## the programs to build + +# we put the stuff that could fail due to bad xine.h locations, etc. at the beginning +# so if the build fails the user knows quickly +app_sources = Split(""" + xineEngine.cpp + xineConfig.cpp + xineScope.c + theStream.cpp + videoWindow.cpp + videoSettings.cpp + captureFrame.cpp + + actions.cpp + stateChange.cpp + slider.cpp + analyzer.cpp + playDialog.cpp + listView.cpp + adjustSizeButton.cpp + fullScreenAction.cpp + insertAspectRatioMenuItems.cpp + playlistFile.cpp + volumeAction.cpp + + ../mxcl.library.cpp + + main.cpp + mainWindow.cpp""") + +KDEprogram( "codeine", app_sources, myenv ) + + +############################ +## Customization + +## Additional include paths for compiling the source files +## Always add '../' (top-level directory) because moc makes code that needs it +KDEaddpaths( ['./', '../', '../../'], myenv ) + +## Necessary libraries to link against +KDEaddlibs( ['qt-mt', 'kio', 'kdecore', 'kdeui', 'xine', 'Xtst'], myenv ) + +## This shows how to add other link flags to the program +myenv['LINKFLAGS'].append('-L/usr/X11R6/lib') + +## If you are using QThread, add this line +# myenv.AppendUnique( CPPFLAGS = ['-DQT_THREAD_SUPPORT'] ) diff --git a/src/app/actions.cpp b/src/app/actions.cpp new file mode 100644 index 0000000..a767a2c --- /dev/null +++ b/src/app/actions.cpp @@ -0,0 +1,27 @@ +// Copyright 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "actions.h" +#include "debug.h" +#include "mxcl.library.h" +#include +#include "xineEngine.h" + +namespace Codeine +{ + PlayAction::PlayAction( QObject *receiver, const char *slot, KActionCollection *ac ) + : KToggleAction( i18n("Play"), "player_play", Qt::Key_Space, receiver, slot, ac, "play" ) + {} + + void + PlayAction::setChecked( bool b ) + { + if( videoWindow()->state() == Engine::Empty && sender() && QCString(sender()->className()) == "KToolBarButton" ) { + // clicking play when empty means open PlayMediaDialog, but we have to uncheck the toolbar button + // as KDElibs sets that checked automatically.. + ((QToolButton*)sender())->setOn( false ); + } + else + KToggleAction::setChecked( b ); + } +} diff --git a/src/app/actions.h b/src/app/actions.h new file mode 100644 index 0000000..4f2f589 --- /dev/null +++ b/src/app/actions.h @@ -0,0 +1,26 @@ +// (c) 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINEACTIONS_H +#define CODEINEACTIONS_H + +#include //baseclass +#include //convenience + +namespace Codeine +{ + KActionCollection *actionCollection(); ///defined in mainWindow.cpp + KAction *action( const char* ); ///defined in mainWindow.cpp + inline KToggleAction *toggleAction( const char *name ) { return (KToggleAction*)action( name ); } + + class PlayAction : public KToggleAction + { + public: + PlayAction( QObject *receiver, const char *slot, KActionCollection* ); + + protected: + virtual void setChecked( bool ); + }; +} + +#endif diff --git a/src/app/adjustSizeButton.cpp b/src/app/adjustSizeButton.cpp new file mode 100644 index 0000000..041b01c --- /dev/null +++ b/src/app/adjustSizeButton.cpp @@ -0,0 +1,125 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "adjustSizeButton.h" +#include "extern.h" +#include +#include +#include +#include +#include +#include +#include "theStream.h" +#include "xineEngine.h" //videoWindow() + + +QString i18n( const char *text ); + +namespace Codeine +{ + AdjustSizeButton::AdjustSizeButton( QWidget *parent ) + : QFrame( parent ) + , m_counter( 0 ) + , m_stage( 1 ) + , m_offset( 0 ) + { + parent->installEventFilter( this ); + + setPalette( QApplication::palette() ); //videoWindow has different palette + setFrameStyle( QFrame::Plain | QFrame::Box ); + + m_preferred = new KPushButton( KGuiItem( i18n("Preferred Scale"), "viewmag" ), this ); + connect( m_preferred, SIGNAL(clicked()), qApp->mainWidget(), SLOT(adjustSize()) ); + connect( m_preferred, SIGNAL(clicked()), SLOT(deleteLater()) ); + + m_oneToOne = new KPushButton( KGuiItem( i18n("Scale 100%"), "viewmag1" ), this ); + connect( m_oneToOne, SIGNAL(clicked()), (QObject*)videoWindow(), SLOT(resetZoom()) ); + connect( m_oneToOne, SIGNAL(clicked()), SLOT(deleteLater()) ); + + QBoxLayout *hbox = new QHBoxLayout( this, 8, 6 ); + QBoxLayout *vbox = new QVBoxLayout( hbox ); + vbox->addWidget( new QLabel( i18n( "Adjust video scale?" ), this ) ); + vbox->addWidget( m_preferred ); + vbox->addWidget( m_oneToOne ); + hbox->addWidget( m_thingy = new QFrame( this ) ); + + m_thingy->setFixedWidth( fontMetrics().width( "X" ) ); + m_thingy->setFrameStyle( QFrame::Plain | QFrame::Box ); + m_thingy->setPaletteForegroundColor( paletteBackgroundColor().dark() ); + + QEvent e( QEvent::Resize ); + eventFilter( 0, &e ); + + adjustSize(); + show(); + + m_timerId = startTimer( 5 ); + } + + void + AdjustSizeButton::timerEvent( QTimerEvent* ) + { + QFrame *&h = m_thingy; + + switch( m_stage ) + { + case 1: //raise + move(); + m_offset++; + + if( m_offset > height() ) + killTimer( m_timerId ), + m_timerId = startTimer( 40 ), + m_stage = 2; + + break; + + case 2: //fill in pause timer bar + if( m_counter < h->height() - 3 ) + QPainter( h ).fillRect( 2, 2, h->width() - 4, m_counter, palette().active().highlight() ); + + if( !hasMouse() ) + m_counter++; + + if( m_counter > h->height() + 5 ) //pause for 360ms before lowering + m_stage = 3, + killTimer( m_timerId ), + m_timerId = startTimer( 6 ); + + break; + + case 3: //lower + if( hasMouse() ) { + m_stage = 1; + m_counter = 0; + m_thingy->repaint(); + break; } + + m_offset--; + move(); + + if( m_offset < 0 ) + deleteLater(); + } + } + + bool + AdjustSizeButton::eventFilter( QObject *o, QEvent *e ) + { + if( e->type() == QEvent::Resize ) { + const QSize preferredSize = TheStream::profile()->readSizeEntry( "Preferred Size" ); + const QSize defaultSize = TheStream::defaultVideoSize(); + const QSize parentSize = parentWidget()->size(); + + m_preferred->setEnabled( preferredSize.isValid() && parentSize != preferredSize && defaultSize != preferredSize ); + m_oneToOne->setEnabled( defaultSize != parentSize ); + + move(); + + if( !m_preferred->isEnabled() && !m_oneToOne->isEnabled() && m_counter == 0 ) + deleteLater(); + } + + return false; + } +} diff --git a/src/app/adjustSizeButton.h b/src/app/adjustSizeButton.h new file mode 100644 index 0000000..6eed27c --- /dev/null +++ b/src/app/adjustSizeButton.h @@ -0,0 +1,37 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_ADJUST_SIZE_BUTTON_H +#define CODEINE_ADJUST_SIZE_BUTTON_H + +#include + +namespace Codeine +{ + class AdjustSizeButton : public QFrame + { + int m_counter; + int m_stage; + int m_offset; + int m_timerId; + + QWidget *m_preferred; + QWidget *m_oneToOne; + + QFrame *m_thingy; + + public: + AdjustSizeButton( QWidget *parent ); + + private: + virtual void timerEvent( QTimerEvent* ); + virtual bool eventFilter( QObject*, QEvent* ); + + inline void move() + { + QWidget::move( parentWidget()->width() - width(), parentWidget()->height() - m_offset ); + } + }; +} + +#endif diff --git a/src/app/analyzer.cpp b/src/app/analyzer.cpp new file mode 100644 index 0000000..c9b8637 --- /dev/null +++ b/src/app/analyzer.cpp @@ -0,0 +1,131 @@ +// (c) 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "analyzer.h" +#include "codeine.h" +#include "debug.h" +#include //interpolate() +#include //event() +#include "xineEngine.h" + +#include "fht.cpp" + +template +Analyzer::Base::Base( QWidget *parent, uint timeout ) + : W( parent, "Analyzer" ) + , m_timeout( timeout ) +{} + +template bool +Analyzer::Base::event( QEvent *e ) +{ + switch( e->type() ) { + case QEvent::Hide: + m_timer.stop(); + break; + + case QEvent::Show: + m_timer.start( timeout() ); + break; + + default: + ; + } + + return QWidget::event( e ); +} + + +Analyzer::Base2D::Base2D( QWidget *parent, uint timeout ) + : Base( parent, timeout ) +{ + setWFlags( Qt::WNoAutoErase ); //no flicker + connect( &m_timer, SIGNAL(timeout()), SLOT(draw()) ); +} + +void +Analyzer::Base2D::draw() +{ + switch( Codeine::engine()->state() ) { + case Engine::Playing: + { + const Engine::Scope &thescope = Codeine::engine()->scope(); + static Analyzer::Scope scope( Analyzer::SCOPE_SIZE ); + + for( int x = 0; x < Analyzer::SCOPE_SIZE; ++x ) + scope[x] = double(thescope[x]) / (1<<15); + + transform( scope ); + analyze( scope ); + + scope.resize( Analyzer::SCOPE_SIZE ); + + bitBlt( this, 0, 0, canvas() ); + break; + } + case Engine::Paused: + break; + + default: + erase(); + } +} + +void +Analyzer::Base2D::resizeEvent( QResizeEvent* ) +{ + m_canvas.resize( size() ); + m_canvas.fill( colorGroup().background() ); +} + + + +// Author: Max Howell , (C) 2003 +// Copyright: See COPYING file that comes with this distribution + +#include + +Analyzer::Block::Block( QWidget *parent ) + : Analyzer::Base2D( parent, 20 ) +{ + setMinimumWidth( 64 ); //-1 is padding, no drawing takes place there + setMaximumWidth( 128 ); + + //TODO yes, do height for width +} + +void +Analyzer::Block::transform( Analyzer::Scope &scope ) //pure virtual +{ + static FHT fht( Analyzer::SCOPE_SIZE_EXP ); + + for( uint x = 0; x < scope.size(); ++x ) + scope[x] *= 2; + + float *front = static_cast( &scope.front() ); + + fht.spectrum( front ); + fht.scale( front, 1.0 / 40 ); +} + +#include +void +Analyzer::Block::analyze( const Analyzer::Scope &s ) +{ + canvas()->fill( colorGroup().foreground().light() ); + + QPainter p( canvas() ); + p.setPen( colorGroup().background() ); + + const double F = double(height()) / (log10( 256 ) * 1.1 /*<- max. amplitude*/); + + for( uint x = 0; x < s.size(); ++x ) + //we draw the blank bit + p.drawLine( x, 0, x, int(height() - log10( s[x] * 256.0 ) * F) ); +} + +int +Analyzer::Block::heightForWidth( int w ) const +{ + return w / 2; +} diff --git a/src/app/analyzer.h b/src/app/analyzer.h new file mode 100644 index 0000000..6fdb12f --- /dev/null +++ b/src/app/analyzer.h @@ -0,0 +1,75 @@ +// (c) 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef ANALYZER_H +#define ANALYZER_H + +#ifdef __FreeBSD__ + #include +#endif + +#include //stack allocated and convenience +#include //stack allocated +#include //baseclass +#include //included for convenience + +namespace Analyzer +{ + typedef std::vector Scope; + + template class Base : public W + { + public: + uint timeout() const { return m_timeout; } + + protected: + Base( QWidget*, uint ); + + virtual void transform( Scope& ) = 0; + virtual void analyze( const Scope& ) = 0; + + private: + virtual bool event( QEvent* ); + + protected: + QTimer m_timer; + uint m_timeout; + }; + + class Base2D : public Base + { + Q_OBJECT + public: + const QPixmap *canvas() const { return &m_canvas; } + + private slots: + void draw(); + + protected: + Base2D( QWidget*, uint timeout ); + + QPixmap *canvas() { return &m_canvas; } + + void paintEvent( QPaintEvent* ) { if( !m_canvas.isNull() ) bitBlt( this, 0, 0, canvas() ); } + void resizeEvent( QResizeEvent* ); + + private: + QPixmap m_canvas; + }; + + class Block : public Analyzer::Base2D + { + public: + Block( QWidget* ); + + protected: + virtual void transform( Analyzer::Scope& ); + virtual void analyze( const Analyzer::Scope& ); + + virtual int heightForWidth( int ) const; + + virtual void show() {} //TODO temporary as the scope plugin causes freezes + }; +} + +#endif diff --git a/src/app/captureFrame.cpp b/src/app/captureFrame.cpp new file mode 100644 index 0000000..4cba5fd --- /dev/null +++ b/src/app/captureFrame.cpp @@ -0,0 +1,296 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "debug.h" +#include +#include +#include +#include +#include +#include "mainWindow.h" +#include "mxcl.library.h" +#include +#include +#include +#include +#include +#include +#include +#include "theStream.h" +#include "xineEngine.h" +#include + + +namespace Codeine { + +class FrameCapturePreview : public KPreviewWidgetBase +{ + QImage m_frame; + + virtual void showPreview( const KURL& ) {} + virtual void clearPreview() {} + + virtual void paintEvent( QPaintEvent* ) + { + QPainter painter( this ); + + const uint h = int( double(m_frame.height()) / m_frame.width() * (width()-5) ); + const uint y = (height() - h) / 2; + painter.drawImage( QRect( 5, y, width(), h ), m_frame ); + + const QString text = QString("%1x%2").arg( m_frame.width() ).arg( m_frame.height() ); + const uint x = (width() - fontMetrics().width( text ))/2; + painter.drawText( x, y + h + fontMetrics().height() + 5, text ); + } + +public: + FrameCapturePreview( const QImage& frame, QWidget *parent ) + : KPreviewWidgetBase( parent ) + , m_frame( frame ) + { + setMinimumWidth( 200 ); + } +}; + + +class FrameCaptureDialog : public QDialog +{ + const QImage m_frame; + const QString m_time; + const QString m_title; + + void message( const QString &text ) { ((MainWindow*)parentWidget())->statusBar()->message( text, 4000 ); } + +public: + FrameCaptureDialog( const QImage &frame, const QString &time, MainWindow *parent ) + : QDialog( parent, 0, false /*modal*/, Qt::WDestructiveClose ) + , m_frame( frame ) + , m_time( time ) + , m_title( TheStream::prettyTitle() ) + { + (new QVBoxLayout( this ))->setAutoAdd( true ); + (new QLabel( this ))->setPixmap( frame ); + + QHBox *box = new QHBox( this ); + KPushButton *o = new KPushButton( KStdGuiItem::save(), box ); + connect( o, SIGNAL(clicked()), SLOT(accept()) ); + + o = new KPushButton( KStdGuiItem::cancel(), box ); + o->setText( i18n("Discard") ); + connect( o, SIGNAL(clicked()), SLOT(reject()) ); + + setCaption( i18n("Capture - %1").arg( time ) ); + setFixedSize( sizeHint() ); + + show(); + + //TODO don't activate + //TODO move to the parent's side - not centrally aligned + } + + ~FrameCaptureDialog() + { + delete [] m_frame.bits(); + } + + virtual void accept() + { + KFileDialog dialog( ":frame_capture", i18n("*.png|PNG Format\n*.jpeg|JPEG Format"), this, 0, false ); + dialog.setOperationMode( KFileDialog::Saving ); + dialog.setCaption( i18n("Save Frame") ); + dialog.setSelection( m_title + " - " + m_time + ".png" ); + dialog.setPreviewWidget( new FrameCapturePreview( m_frame, &dialog ) ); + + if( dialog.exec() == Accepted ) { + const QString fileName = dialog.selectedFile(); + if( fileName.isEmpty() ) + return; + + const QString type = dialog.currentFilter().remove( 0, 2 ).upper(); + if( m_frame.save( fileName, type ) ) + message( i18n("%1 saved successfully").arg( fileName ) ); + else + message( i18n("Sorry, could not save %1").arg( fileName ) ); + } + + deleteLater(); + } +}; + + +void +MainWindow::captureFrame() +{ + new FrameCaptureDialog( videoWindow()->captureFrame(), m_timeLabel->text(), this ); +} + + +/************************************************************ + * Helpers to convert yuy and yv12 frames to rgb * + * code from gxine modified for 32bit output * + * Copyright (C) 2000-2003 the xine project * + ************************************************************/ + +static void +yuy2Toyv12( uint8_t *y, uint8_t *u, uint8_t *v, uint8_t *input, int w, int h ) +{ + const int w2 = w / 2; + for( int j, i = 0; i < h; i += 2 ) { + for( j = 0; j < w2; j++ ) + { + // packed YUV 422 is: Y[i] U[i] Y[i+1] V[i] + *(y++) = *(input++); + *(u++) = *(input++); + *(y++) = *(input++); + *(v++) = *(input++); + } + + // down sampling + for( j = 0; j < w2; j++ ) { + // skip every second line for U and V + *(y++) = *(input++); + input++; + *(y++) = *(input++); + input++; + } + } +} + +static uchar* +yv12ToRgb( uint8_t *src_y, uint8_t *src_u, uint8_t *src_v, const int w, const int h ) +{ + /// Create rgb data from yv12 + + #define clip_8_bit(val) \ + { \ + if( val < 0 ) \ + val = 0; \ + else if( val > 255 ) \ + val = 255; \ + } + + int y, u, v; + int r, g, b; + + int sub_i_uv; + int sub_j_uv; + + const int uv_width = w / 2; + const int uv_height = h / 2; + + uchar * const rgb = new uchar[(w * h * 4)]; //qt needs a 32bit align + if( !rgb ) + return 0; + + for( int i = 0; i < h; ++i ) { + // calculate u & v rows + sub_i_uv = ((i * uv_height) / h); + + for( int j = 0; j < w; ++j ) { + // calculate u & v columns + sub_j_uv = (j * uv_width) / w; + + /*************************************************** + * + * Colour conversion from + * http://www.inforamp.net/~poynton/notes/colour_and_gamma/ColorFAQ.html#RTFToC30 + * + * Thanks to Billy Biggs + * for the pointer and the following conversion. + * + * R' = [ 1.1644 0 1.5960 ] ([ Y' ] [ 16 ]) + * G' = [ 1.1644 -0.3918 -0.8130 ] * ([ Cb ] - [ 128 ]) + * B' = [ 1.1644 2.0172 0 ] ([ Cr ] [ 128 ]) + * + * Where in xine the above values are represented as + * + * Y' == image->y + * Cb == image->u + * Cr == image->v + * + ***************************************************/ + + y = src_y[(i * w) + j] - 16; + u = src_u[(sub_i_uv * uv_width) + sub_j_uv] - 128; + v = src_v[(sub_i_uv * uv_width) + sub_j_uv] - 128; + + r = (int)((1.1644 * (double)y) + (1.5960 * (double)v)); + g = (int)((1.1644 * (double)y) - (0.3918 * (double)u) - (0.8130 * (double)v)); + b = (int)((1.1644 * (double)y) + (2.0172 * (double)u)); + + clip_8_bit( r ); + clip_8_bit( g ); + clip_8_bit( b ); + + rgb[(i * w + j) * 4 + 0] = b; + rgb[(i * w + j) * 4 + 1] = g; + rgb[(i * w + j) * 4 + 2] = r; + rgb[(i * w + j) * 4 + 3] = 0; + } + } + + return rgb; +} + +/************************************************************/ + + +QImage +VideoWindow::captureFrame() const +{ + DEBUG_BLOCK + + int ratio, format, w, h; + if( !xine_get_current_frame( *engine(), &w, &h, &ratio, &format, NULL ) ) + return QImage(); + + uint8_t *yuv = new uint8_t[((w+8) * (h+1) * 2)]; + if( yuv == 0 ) { + Debug::error() << "Not enough memory to make screenframe!\n"; + return QImage(); } + + xine_get_current_frame( *engine(), &w, &h, &ratio, &format, yuv ); + + // convert to yv12 if necessary + uint8_t *y = 0, *u = 0, *v = 0; + switch( format ) + { + case XINE_IMGFMT_YUY2: { + uint8_t *yuy2 = yuv; + + yuv = new uint8_t[(w * h * 2)]; + if( yuv == 0 ) { + Debug::error() << "Not enough memory to make screenframe!\n"; + delete [] yuy2; + return QImage(); } + + y = yuv; + u = yuv + w * h; + v = yuv + w * h * 5 / 4; + + yuy2Toyv12( y, u, v, yuy2, w, h ); + + delete [] yuy2; + } break; + + case XINE_IMGFMT_YV12: + y = yuv; + u = yuv + w * h; + v = yuv + w * h * 5 / 4; + break; + + default: + Debug::warning() << "Format " << format << " not supported!\n"; + delete [] yuv; + return QImage(); + } + + // convert to rgb + uchar *rgb = yv12ToRgb( y, u, v, w, h ); + QImage frame( rgb, w, h, 32, 0, 0, QImage::IgnoreEndian ); + delete [] yuv; + + return frame; +} + +} diff --git a/src/app/config.h b/src/app/config.h new file mode 100644 index 0000000..fbab5e8 --- /dev/null +++ b/src/app/config.h @@ -0,0 +1,20 @@ +// (c) 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINECONFIG_H +#define CODEINECONFIG_H + +#include +#include + +namespace Codeine +{ + static inline KConfig *config( const QString &group ) + { + KConfig* const instance = KGlobal::config(); + instance->setGroup( group ); + return instance; + } +} + +#endif diff --git a/src/app/extern.h b/src/app/extern.h new file mode 100644 index 0000000..20e49fd --- /dev/null +++ b/src/app/extern.h @@ -0,0 +1,28 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_EXTERN_H +#define CODEINE_EXTERN_H + +extern "C" +{ + typedef struct xine_s xine_t; +} + +class QPopupMenu; +class QWidget; + +namespace Codeine +{ + class VideoWindow; + class XineEngine; + + VideoWindow* const engine(); //defined in xineEngine.h + VideoWindow* const videoWindow(); //defined in xineEngine.h + + void showVideoSettingsDialog( QWidget* ); + void showXineConfigurationDialog( QWidget*, xine_t* ); + void insertAspectRatioMenuItems( QPopupMenu* ); +} + +#endif diff --git a/src/app/fht.cpp b/src/app/fht.cpp new file mode 100644 index 0000000..4d03851 --- /dev/null +++ b/src/app/fht.cpp @@ -0,0 +1,262 @@ +// FHT - Fast Hartley Transform Class +// +// Copyright (C) 2004 Melchior FRANZ - mfranz@kde.org +// +// 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, 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA +// +// $Id: fht.cpp,v 1.3 2004/06/05 20:20:36 mfranz Exp $ + +#include +#include +#include "fht.h" + + +FHT::FHT(int n) : + m_buf(0), + m_tab(0), + m_log(0) +{ + if (n < 3) { + m_num = 0; + m_exp2 = -1; + return; + } + m_exp2 = n; + m_num = 1 << n; + if (n > 3) { + m_buf = new float[m_num]; + m_tab = new float[m_num * 2]; + makeCasTable(); + } +} + + +FHT::~FHT() +{ + delete[] m_buf; + delete[] m_tab; + delete[] m_log; +} + + +void FHT::makeCasTable(void) +{ + float d, *costab, *sintab; + int ul, ndiv2 = m_num / 2; + + for (costab = m_tab, sintab = m_tab + m_num / 2 + 1, ul = 0; ul < m_num; ul++) { + d = M_PI * ul / ndiv2; + *costab = *sintab = cos(d); + + costab += 2, sintab += 2; + if (sintab > m_tab + m_num * 2) + sintab = m_tab + 1; + } +} + + +float* FHT::copy(float *d, float *s) +{ + return (float *)memcpy(d, s, m_num * sizeof(float)); +} + + +float* FHT::clear(float *d) +{ + return (float *)memset(d, 0, m_num * sizeof(float)); +} + + +void FHT::scale(float *p, float d) +{ + for (int i = 0; i < (m_num / 2); i++) + *p++ *= d; +} + + +void FHT::ewma(float *d, float *s, float w) +{ + for (int i = 0; i < (m_num / 2); i++, d++, s++) + *d = *d * w + *s * (1 - w); +} + + +static inline float sind(float d) { return sin(d * M_PI / 180); } +void FHT::pattern(float *p, bool rect = false) +{ + static float f = 1.0; + static float h = 0.1; + int i; + for (i = 0; i < 3 * m_num / 4; i++, p++) { + float o = 360.0 * i / m_num; + *p = sind(f * o); + if (rect) + *p = *p < 0 ? -1.0 : 1.0; + } + for (; i < m_num; i++) + *p++ = 0.0; + if (f > m_num / 2.0 || f < .05) + h = -h; + f += h; +} + + +void FHT::logSpectrum(float *out, float *p) +{ + int n = m_num / 2, i, j, k, *r; + if (!m_log) { + m_log = new int[n]; + float f = n / log10(n); + for (i = 0, r = m_log; i < n; i++, r++) { + j = int(rint(log10(i + 1.0) * f)); + *r = j >= n ? n - 1 : j; + } + } + semiLogSpectrum(p); + *out++ = *p = *p / 100; + for (k = i = 1, r = m_log; i < n; i++) { + j = *r++; + if (i == j) + *out++ = p[i]; + else { + float base = p[k - 1]; + float step = (p[j] - base) / (j - (k - 1)); + for (float corr = 0; k <= j; k++, corr += step) + *out++ = base + corr; + } + } +} + + +void FHT::semiLogSpectrum(float *p) +{ + float e; + power2(p); + for (int i = 0; i < (m_num / 2); i++, p++) { + e = 10.0 * log10(sqrt(*p * .5)); + *p = e < 0 ? 0 : e; + } +} + + +void FHT::spectrum(float *p) +{ + power2(p); + for (int i = 0; i < (m_num / 2); i++, p++) + *p = (float)sqrt(*p * .5); +} + + +void FHT::power(float *p) +{ + power2(p); + for (int i = 0; i < (m_num / 2); i++) + *p++ *= .5; +} + + +void FHT::power2(float *p) +{ + int i; + float *q; + _transform(p, m_num, 0); + + *p = (*p * *p), *p += *p, p++; + + for (i = 1, q = p + m_num - 2; i < (m_num / 2); i++, --q) + *p++ = (*p * *p) + (*q * *q); +} + + +void FHT::transform(float *p) +{ + if (m_num == 8) + transform8(p); + else + _transform(p, m_num, 0); +} + + +void FHT::transform8(float *p) +{ + float a, b, c, d, e, f, g, h, b_f2, d_h2; + float a_c_eg, a_ce_g, ac_e_g, aceg, b_df_h, bdfh; + + a = *p++, b = *p++, c = *p++, d = *p++; + e = *p++, f = *p++, g = *p++, h = *p; + b_f2 = (b - f) * M_SQRT2; + d_h2 = (d - h) * M_SQRT2; + + a_c_eg = a - c - e + g; + a_ce_g = a - c + e - g; + ac_e_g = a + c - e - g; + aceg = a + c + e + g; + + b_df_h = b - d + f - h; + bdfh = b + d + f + h; + + *p = a_c_eg - d_h2; + *--p = a_ce_g - b_df_h; + *--p = ac_e_g - b_f2; + *--p = aceg - bdfh; + *--p = a_c_eg + d_h2; + *--p = a_ce_g + b_df_h; + *--p = ac_e_g + b_f2; + *--p = aceg + bdfh; +} + + +void FHT::_transform(float *p, int n, int k) +{ + if (n == 8) { + transform8(p + k); + return; + } + + int i, j, ndiv2 = n / 2; + float a, *t1, *t2, *t3, *t4, *ptab, *pp; + + for (i = 0, t1 = m_buf, t2 = m_buf + ndiv2, pp = &p[k]; i < ndiv2; i++) + *t1++ = *pp++, *t2++ = *pp++; + + memcpy(p + k, m_buf, sizeof(float) * n); + + _transform(p, ndiv2, k); + _transform(p, ndiv2, k + ndiv2); + + j = m_num / ndiv2 - 1; + t1 = m_buf; + t2 = t1 + ndiv2; + t3 = p + k + ndiv2; + ptab = m_tab; + pp = p + k; + + a = *ptab++ * *t3++; + a += *ptab * *pp; + ptab += j; + + *t1++ = *pp + a; + *t2++ = *pp++ - a; + + for (i = 1, t4 = p + k + n; i < ndiv2; i++, ptab += j) { + a = *ptab++ * *t3++; + a += *ptab * *--t4; + + *t1++ = *pp + a; + *t2++ = *pp++ - a; + } + memcpy(p + k, m_buf, sizeof(float) * n); +} + diff --git a/src/app/fht.h b/src/app/fht.h new file mode 100644 index 0000000..3dc5387 --- /dev/null +++ b/src/app/fht.h @@ -0,0 +1,126 @@ +// FHT - Fast Hartley Transform Class +// +// Copyright (C) 2004 Melchior FRANZ - mfranz@kde.org +// +// 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, 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA +// +// $Id: fht.h,v 1.3 2004/06/05 20:20:36 mfranz Exp $ + +#ifndef FHT_H +#define FHT_H + +/** + * Implementation of the Hartley Transform after Bracewell's discrete + * algorithm. The algorithm is subject to US patent No. 4,646,256 (1987) + * but was put into public domain by the Board of Trustees of Stanford + * University in 1994 and is now freely available[1]. + * + * [1] Computer in Physics, Vol. 9, No. 4, Jul/Aug 1995 pp 373-379 + */ +class FHT +{ + int m_exp2; + int m_num; + float *m_buf; + float *m_tab; + int *m_log; + + /** + * Create a table of CAS (cosine and sine) values. + * Has only to be done in the constructor and saves from + * calculating the same values over and over while transforming. + */ + void makeCasTable(); + + /** + * Recursive in-place Hartley transform. For internal use only! + */ + void _transform(float *, int, int); + + public: + /** + * Prepare transform for data sets with @f$2^n@f$ numbers, whereby @f$n@f$ + * should be at least 3. Values of more than 3 need a trigonometry table. + * @see makeCasTable() + */ + FHT(int); + + ~FHT(); + inline int sizeExp() const { return m_exp2; } + inline int size() const { return m_num; } + float *copy(float *, float *); + float *clear(float *); + void scale(float *, float); + + /** + * Exponentially Weighted Moving Average (EWMA) filter. + * @param d is the filtered data. + * @param s is fresh input. + * @param w is the weighting factor. + */ + void ewma(float *d, float *s, float w); + + /** + * Test routine to create wobbling sine or rectangle wave. + * @param d destination vector. + * @param rect rectangle if true, sine otherwise. + */ + void pattern(float *d, bool rect); + + /** + * Logarithmic audio spectrum. Maps semi-logarithmic spectrum + * to logarithmic frequency scale, interpolates missing values. + * A logarithmic index map is calculated at the first run only. + * @param p is the input array. + * @param out is the spectrum. + */ + void logSpectrum(float *out, float *p); + + /** + * Semi-logarithmic audio spectrum. + */ + void semiLogSpectrum(float *); + + /** + * Fourier spectrum. + */ + void spectrum(float *); + + /** + * Calculates a mathematically correct FFT power spectrum. + * If further scaling is applied later, use power2 instead + * and factor the 0.5 in the final scaling factor. + * @see FHT::power2() + */ + void power(float *); + + /** + * Calculates an FFT power spectrum with doubled values as a + * result. The values need to be multiplied by 0.5 to be exact. + * Note that you only get @f$2^{n-1}@f$ power values for a data set + * of @f$2^n@f$ input values. + * @see FHT::power() + */ + void power2(float *); + + /** + * Discrete Hartley transform of data sets with 8 values. + */ + void transform8(float *); + + void transform(float *); +}; + +#endif diff --git a/src/app/fullScreenAction.cpp b/src/app/fullScreenAction.cpp new file mode 100644 index 0000000..f28da84 --- /dev/null +++ b/src/app/fullScreenAction.cpp @@ -0,0 +1,96 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "extern.h" +#include "fullScreenAction.h" +#include +#include +#include +#include "xineEngine.h" //videoWindow() + + +FullScreenAction::FullScreenAction( QWidget* window, KActionCollection *parent ) + : KToggleAction( QString::null, Key_F, 0, 0, parent, "fullscreen" ) + , m_window( window ) + , m_shouldBeDisabled( false ) + , m_state( 0 ) +{ + window->installEventFilter( this ); + setChecked( false ); +} + +void +FullScreenAction::setChecked( bool setChecked ) +{ + KToggleAction::setChecked( setChecked ); + + m_window->raise(); + + const int id = m_window->winId(); + if( setChecked ) { + setText( i18n("Exit F&ull Screen Mode") ); + setIcon("window_nofullscreen"); + m_state = KWin::windowInfo( id ).state(); + KWin::setState( id, NET::FullScreen ); + } + else { + setText(i18n("F&ull Screen Mode")); + setIcon("window_fullscreen"); + KWin::clearState( id, NET::FullScreen ); + KWin::setState( id, m_state ); // get round bug in KWin where it forgets maximisation state + } + + if( setChecked == false && m_shouldBeDisabled ) + setEnabled( false ); +} + +void +FullScreenAction::setEnabled( bool setEnabled ) +{ + if( setEnabled == false && isChecked() ) + // don't disable the action if we are currently in fullscreen mode + // as then the user can't exit fullscreen mode! Instead disable it + // when we next get toggled out of fullscreen mode + m_shouldBeDisabled = true; + + else { + //FIXME Codeine specific (because videoWindow isn't the window we control, we control the KMainWindow) + //NOTE also if the videoWindow is hidden at some point, this is broken.. + //TODO new type of actionclass that event filters and is always correct state + if( setEnabled && reinterpret_cast(Codeine::videoWindow())->isHidden() ) + setEnabled = false; + + m_shouldBeDisabled = false; + KToggleAction::setEnabled( setEnabled ); + } +} + +bool +FullScreenAction::eventFilter( QObject *o, QEvent *e ) +{ + if( o == m_window ) + switch( e->type() ) { + #if QT_VERSION >= 0x030300 + case QEvent::WindowStateChange: + #else + case QEvent::ShowFullScreen: + case QEvent::ShowNormal: + case QEvent::ShowMaximized: + case QEvent::ShowMinimized: + #endif + if (m_window->isFullScreen() != isChecked()) + slotActivated(); // setChecked( window->isFullScreen()) wouldn't emit signals + + if (m_window->isFullScreen() && !isEnabled()) { + m_shouldBeDisabled = true; + setEnabled( true ); + } + + break; + + default: + ; + } + + return false; +} diff --git a/src/app/fullScreenAction.h b/src/app/fullScreenAction.h new file mode 100644 index 0000000..4234633 --- /dev/null +++ b/src/app/fullScreenAction.h @@ -0,0 +1,27 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include + + +/** + * @class FullSCreenAction + * @author Max Howell + * @short Adapted KToggleFullScreenAction, mainly because that class is shit + */ +class FullScreenAction : public KToggleAction +{ +public: + FullScreenAction( QWidget *window, KActionCollection* ); + + virtual void setChecked( bool ); + virtual void setEnabled( bool ); + +protected: + virtual bool eventFilter( QObject* o, QEvent* e ); + +private: + QWidget *m_window; + bool m_shouldBeDisabled; + unsigned long m_state; +}; diff --git a/src/app/insertAspectRatioMenuItems.cpp b/src/app/insertAspectRatioMenuItems.cpp new file mode 100644 index 0000000..353fe43 --- /dev/null +++ b/src/app/insertAspectRatioMenuItems.cpp @@ -0,0 +1,24 @@ +// Copyright 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include +#include + +QString i18n( const char *text ); + + +namespace Codeine +{ + void + insertAspectRatioMenuItems( QPopupMenu *menu ) + { + menu->insertItem( i18n( "Determine &Automatically" ), XINE_VO_ASPECT_AUTO ); + menu->insertSeparator(); + menu->insertItem( i18n( "&Square (1:1)" ), XINE_VO_ASPECT_SQUARE ); + menu->insertItem( i18n( "&4:3" ), XINE_VO_ASPECT_4_3 ); + menu->insertItem( i18n( "Ana&morphic (16:9)" ), XINE_VO_ASPECT_ANAMORPHIC ); + menu->insertItem( i18n( "&DVB (2.11:1)" ), XINE_VO_ASPECT_DVB ); + + menu->setItemChecked( XINE_VO_ASPECT_AUTO, true ); + } +} diff --git a/src/app/listView.cpp b/src/app/listView.cpp new file mode 100644 index 0000000..b7990ec --- /dev/null +++ b/src/app/listView.cpp @@ -0,0 +1,39 @@ +// (c) 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINELISTVIEW_CPP +#define CODEINELISTVIEW_CPP + +#include + +namespace Codeine +{ + class ListView : public KListView + { + public: + ListView( QWidget *parent ) : KListView( parent ) + { + addColumn( QString::null, 0 ); + addColumn( QString::null ); + + setResizeMode( LastColumn ); + setMargin( 2 ); + setSorting( -1 ); + setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum ); + setAllColumnsShowFocus( true ); + setItemMargin( 3 ); + } + + virtual QSize sizeHint() const + { + const QSize sh = KListView::sizeHint(); + + return QSize( sh.width(), + childCount() == 0 + ? 50 + : QMIN( sh.height(), childCount() * (firstChild()->height()) + margin() * 2 + 4 + reinterpret_cast(header())->height() ) ); + } + }; +} + +#endif diff --git a/src/app/main.cpp b/src/app/main.cpp new file mode 100644 index 0000000..7c0f6fc --- /dev/null +++ b/src/app/main.cpp @@ -0,0 +1,52 @@ +// (c) 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "codeine.h" +#include +#include +#include +#include "mainWindow.h" +#include + + +static KAboutData aboutData( APP_NAME, + I18N_NOOP(PRETTY_NAME), APP_VERSION, + I18N_NOOP("A video player that has a usability focus"), KAboutData::License_GPL_V2, + I18N_NOOP("Copyright 2006, Max Howell"), 0, + "http://www.methylblue.com/codeine/", + "codeine@methylblue.com" ); + +static const KCmdLineOptions options[] = { + { "+[URL]", I18N_NOOP( "Play 'URL'" ), 0 }, + { "play-dvd", I18N_NOOP( "Play DVD Video" ), 0 }, + { 0, 0, 0 } }; + +int +main( int argc, char **argv ) +{ + //we need to do this, says adrianS from SuSE + if( !XInitThreads() ) + return 1; + + aboutData.addCredit( "Mike Diehl", I18N_NOOP("Handbook") ); + aboutData.addCredit( "The Kaffeine Developers", I18N_NOOP("Great reference code") ); + aboutData.addCredit( "Eric Prydz", I18N_NOOP("The video for \"Call on Me\" encouraged plenty of debugging! ;)") ); + aboutData.addCredit( "David Vignoni", I18N_NOOP("The current Codeine icon") ); + aboutData.addCredit( "Ian Monroe", I18N_NOOP("Patches, advice and moral support") ); + + + KCmdLineArgs::init( argc, argv, &aboutData ); + KCmdLineArgs::addCmdLineOptions( options ); + + KApplication application; + int returnValue; + + { + Codeine::MainWindow mainWindow; + mainWindow.show(); + + returnValue = application.exec(); + } + + return returnValue; +} diff --git a/src/app/mainWindow.cpp b/src/app/mainWindow.cpp new file mode 100644 index 0000000..856e0b6 --- /dev/null +++ b/src/app/mainWindow.cpp @@ -0,0 +1,714 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "actions.h" +#include "analyzer.h" +#include "config.h" +#include "configure.h" +#include +#include "debug.h" +#include "extern.h" //dialog creation function definitions +#include "fullScreenAction.h" +#include +#include +#include +#include //::open() +#include //::timerEvent() +#include +#include +#include +#include +#include +#include +#include "mainWindow.h" +#include "playDialog.h" //::play() +#include "playlistFile.h" +#include "mxcl.library.h" +#include +#include +#include //::stateChanged() +#include //ctor +#include //because XMLGUI is poorly designed +#include +#include "slider.h" +#include "theStream.h" +#include "volumeAction.h" +#include "xineEngine.h" + +#ifndef NO_XTEST_EXTENSION +extern "C" +{ + #include + #include +} +#endif + + +namespace Codeine { + + + /// @see codeine.h + QWidget *mainWindow() { return kapp->mainWidget(); } + + +MainWindow::MainWindow() + : KMainWindow() + , m_positionSlider( new Slider( this, 65535 ) ) + , m_timeLabel( new QLabel( " 0:00:00 ", this ) ) + , m_titleLabel( new KSqueezedTextLabel( this ) ) +{ + DEBUG_BLOCK + + clearWFlags( WDestructiveClose ); //we are allocated on the stack + + kapp->setMainWidget( this ); + + new VideoWindow( this ); + setCentralWidget( videoWindow() ); + setFocusProxy( videoWindow() ); // essential! See VideoWindow::event(), QEvent::FocusOut + + // these have no affect beccause "KDE Knows Best" FFS + setDockEnabled( toolBar(), Qt::DockRight, false ); //doesn't make sense due to our large horizontal slider + setDockEnabled( toolBar(), Qt::DockLeft, false ); //as above + + m_titleLabel->setMargin( 2 ); + m_timeLabel->setFont( KGlobalSettings::fixedFont() ); + m_timeLabel->setAlignment( AlignCenter ); + m_timeLabel->setMinimumSize( m_timeLabel->sizeHint() ); + + // work around a bug in KStatusBar + // sizeHint width of statusbar seems to get stupidly large quickly + statusBar()->setSizePolicy( QSizePolicy::Ignored, QSizePolicy::Maximum ); + + statusBar()->addWidget( m_titleLabel, 1, false ); + statusBar()->addWidget( m_analyzer = new Analyzer::Block( this ), 0, true ); + statusBar()->addWidget( m_timeLabel, 0, true ); + setupActions(); + setupGUI(); + setStandardToolBarMenuEnabled( false ); //bah to setupGUI()! + toolBar()->show(); //it's possible it would be hidden, but we don't want that as no UI way to show it! + + // only show dvd button when playing a dvd + { + struct KdeIsTehSuck : public QObject + { + virtual bool eventFilter( QObject*, QEvent *e ) + { + if (e->type() != QEvent::LayoutHint) + return false; + + // basically, KDE shows all tool-buttons, even if they are + // hidden after it does any layout operation. Yay for KDE. Yay. + QWidget *button = (QWidget*)((KMainWindow*)mainWindow())->toolBar()->child( "toolbutton_toggle_dvd_menu" ); + if (button) + button->setShown( TheStream::url().protocol() == "dvd" ); + return false; + } + } *o; + o = new KdeIsTehSuck; + toolBar()->installEventFilter( o ); + insertChild( o ); + } + + { + QPopupMenu *menu = 0, *settings = static_cast(factory()->container( "settings", this )); + int id = SubtitleChannelsMenuItemId, index = 0; + + #define make_menu( name, text ) \ + menu = new QPopupMenu( this, name ); \ + menu->setCheckable( true ); \ + connect( menu, SIGNAL(activated( int )), engine(), SLOT(setStreamParameter( int )) ); \ + connect( menu, SIGNAL(aboutToShow()), SLOT(aboutToShowMenu()) ); \ + settings->insertItem( text, menu, id, index ); \ + settings->setItemEnabled( id, false ); \ + id++, index++; + + make_menu( "subtitle_channels_menu", i18n( "&Subtitles" ) ); + make_menu( "audio_channels_menu", i18n( "A&udio Channels" ) ); + make_menu( "aspect_ratio_menu", i18n( "Aspect &Ratio" ) ); + #undef make_menu + + Codeine::insertAspectRatioMenuItems( menu ); //so we don't have to include xine.h here + + settings->insertSeparator( index ); + } + + QObjectList *list = toolBar()->queryList( "KToolBarButton" ); + if (list->isEmpty()) { + MessageBox::error( i18n( + "" PRETTY_NAME " could not load its interface, this probably means that " PRETTY_NAME " is not " + "installed to the correct prefix. If you installed from packages please contact the packager, if " + "you installed from source please try running the configure script again like this: " + "
 % ./configure --prefix=`kde-config --prefix`
" ) ); + + std::exit( 1 ); + } + delete list; + + KXMLGUIClient::stateChanged( "empty" ); + + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + if( args->count() || args->isSet( "play-dvd" ) || kapp->isRestored() ) + //we need to resize the window, so we can't show the window yet + init(); + else { + //"faster" startup + //TODO if we have a size stored for this video, do the "faster" route + QTimer::singleShot( 0, this, SLOT(init()) ); + QApplication::setOverrideCursor( KCursor::waitCursor() ); } +} + +void +MainWindow::init() +{ + DEBUG_BLOCK + + connect( engine(), SIGNAL(statusMessage( const QString& )), this, SLOT(engineMessage( const QString& )) ); + connect( engine(), SIGNAL(stateChanged( Engine::State )), this, SLOT(engineStateChanged( Engine::State )) ); + connect( engine(), SIGNAL(channelsChanged( const QStringList& )), this, SLOT(setChannels( const QStringList& )) ); + connect( engine(), SIGNAL(titleChanged( const QString& )), m_titleLabel, SLOT(setText( const QString& )) ); + connect( m_positionSlider, SIGNAL(valueChanged( int )), this, SLOT(showTime( int )) ); + + if( !engine()->init() ) { + KMessageBox::error( this, i18n( + "xine could not be successfully initialised. " PRETTY_NAME " will now exit. " + "You can try to identify what is wrong with your xine installation using the xine-check command at a command-prompt.") ); + std::exit( 2 ); + } + + //would be dangerous for these to65535 happen before the videoWindow() is initialised + setAcceptDrops( true ); + connect( m_positionSlider, SIGNAL(sliderReleased( uint )), engine(), SLOT(seek( uint )) ); + connect( statusBar(), SIGNAL(messageChanged( const QString& )), engine(), SLOT(showOSD( const QString& )) ); + + QApplication::restoreOverrideCursor(); + + if( !kapp->isRestored() ) { + KCmdLineArgs &args = *KCmdLineArgs::parsedArgs(); + if (args.isSet( "play-dvd" )) + open( "dvd:/" ); + else if (args.count() > 0 ) { + open( args.url( 0 ) ); + args.clear(); + adjustSize(); //will resize us to reflect the videoWindow's sizeHint() + } + else + //show the welcome dialog + playMedia( true ); // true = show in style of welcome dialog + } + else + //session management must be done after the videoWindow() has been initialised + restore( 1, false ); + + //don't do until videoWindow() is initialised! + startTimer( 50 ); +} + +MainWindow::~MainWindow() +{ + DEBUG_FUNC_INFO + + hide(); //so we appear to have quit, and then sound fades out below + + delete videoWindow(); //fades out sound in dtor +} + +bool +MainWindow::queryExit() +{ + if( toggleAction( "fullscreen" )->isChecked() ) { + // there seems to be no other way to stop KMainWindow + // saving the window state without any controls + fullScreenToggled( false ); + showNormal(); + QApplication::sendPostedEvents( this, 0 ); + // otherwise KMainWindow saves the screensize as maximised + Codeine::MessageBox::sorry( + "This annoying messagebox is to get round a bug in either KDE or Qt. " + "Just press OK and Codeine will quit." ); + //NOTE not actually needed + saveAutoSaveSettings(); + hide(); + } + + return true; +} + +void +MainWindow::setupActions() +{ + DEBUG_BLOCK + + KActionCollection * const ac = actionCollection(); + + KStdAction::quit( kapp, SLOT(quit()), ac ); + KStdAction::open( this, SLOT(playMedia()), ac, "play_media" )->setText( i18n("Play &Media...") ); + connect( new FullScreenAction( this, ac ), SIGNAL(toggled( bool )), SLOT(fullScreenToggled( bool )) ); + + new PlayAction( this, SLOT(play()), ac ); + new KAction( i18n("Stop"), "player_stop", Key_S, engine(), SLOT(stop()), ac, "stop" ); + + new KToggleAction( i18n("Record"), "player_record", CTRL + Key_R, engine(), SLOT(record()), ac, "record" ); + + new KAction( i18n("Reset Video Scale"), "viewmag1", Key_Equal, videoWindow(), SLOT(resetZoom()), ac, "reset_zoom" ); + new KAction( i18n("Media Information"), "messagebox_info", Key_I, this, SLOT(streamInformation()), ac, "information" ); + new KAction( i18n("Menu Toggle"), "dvd_unmount", Key_R, engine(), SLOT(toggleDVDMenu()), ac, "toggle_dvd_menu" ); + new KAction( i18n("&Capture Frame"), "frame_image", Key_C, this, SLOT(captureFrame()), ac, "capture_frame" ); + + new KAction( i18n("Video Settings..."), "configure", Key_V, this, SLOT(configure()), ac, "video_settings" ); + new KAction( i18n("Configure xine..."), "configure", 0, this, SLOT(configure()), ac, "xine_settings" ); + + (new KWidgetAction( m_positionSlider, i18n("Position Slider"), 0, 0, 0, ac, "position_slider" ))->setAutoSized( true ); + + new VolumeAction( toolBar(), ac ); +} + +void +MainWindow::saveProperties( KConfig *config ) +{ + config->writeEntry( "url", TheStream::url().url() ); + config->writeEntry( "time", engine()->time() ); +} + +void +MainWindow::readProperties( KConfig *config ) +{ + if( engine()->load( config->readPathEntry( "url" ) ) ) + engine()->play( config->readNumEntry( "time" ) ); +} + +void +MainWindow::timerEvent( QTimerEvent* ) +{ + static int counter = 0; + + if( engine()->state() == Engine::Playing ) { + ++counter &= 1023; + + m_positionSlider->setValue( engine()->position() ); + if( !m_positionSlider->isEnabled() && counter % 10 == 0 ) // 0.5 seconds + // usually the slider emits a signal that updates the timeLabel + // but not if the slider isn't moving because there is no length + showTime(); + + #ifndef NO_XTEST_EXTENSION + if( counter == 0 /*1020*/ ) { // 51 seconds //do at 0 to ensure screensaver doesn't happen before 51 seconds is up (somehow) + const bool isOnThisDesktop = KWin::windowInfo( winId() ).isOnDesktop( KWin::currentDesktop() ); + + if( videoWindow()->isVisible() && isOnThisDesktop ) { + int key = XKeysymToKeycode( x11Display(), XK_Shift_R ); + + XTestFakeKeyEvent( x11Display(), key, true, CurrentTime ); + XTestFakeKeyEvent( x11Display(), key, false, CurrentTime ); + XSync( x11Display(), false ); + } + } + #endif + } +} + +void +MainWindow::showTime( int pos ) +{ + #define zeroPad( n ) n < 10 ? QString("0%1").arg( n ) : QString::number( n ) + + const int ms = (pos == -1) ? engine()->time() : int(engine()->length() * (pos / 65535.0)); + const int s = ms / 1000; + const int m = s / 60; + const int h = m / 60; + + QString time = zeroPad( s % 60 ); //seconds + time.prepend( ':' ); + time.prepend( zeroPad( m % 60 ) ); //minutes + time.prepend( ':' ); + time.prepend( QString::number( h ) ); //hours + + m_timeLabel->setText( time ); +} + +void +MainWindow::engineMessage( const QString &message ) +{ + statusBar()->message( message, 3500 ); +} + +bool +MainWindow::open( const KURL &url ) +{ + DEBUG_BLOCK + debug() << url << endl; + + if( load( url ) ) { + const int offset = TheStream::hasProfile() + // adjust offset if we have session history for this video + ? TheStream::profile()->readNumEntry( "Position", 0 ) + : 0; + + return engine()->play( offset ); + } + + return false; +} + +bool +MainWindow::load( const KURL &url ) +{ + //FileWatch the file that is opened + + if( url.isEmpty() ) { + MessageBox::sorry( i18n( "Codeine was asked to open an empty URL; it cannot." ) ); + return false; + } + + PlaylistFile playlist( url ); + if( playlist.isPlaylist() ) { + //TODO: problem is we return out of the function + //statusBar()->message( i18n("Parsing playlist file...") ); + + if( playlist.isValid() ) + return engine()->load( playlist.firstUrl() ); + else { + MessageBox::sorry( playlist.error() ); + return false; + } + } + + if (url.protocol() == "media") { + #define UDS_LOCAL_PATH (72 | KIO::UDS_STRING) + KIO::UDSEntry e; + if (!KIO::NetAccess::stat( url, e, 0 )) + MessageBox::sorry( "There was an internal error with the media slave..." ); + else { + KIO::UDSEntry::ConstIterator end = e.end(); + for (KIO::UDSEntry::ConstIterator it = e.begin(); it != end; ++it) + if ((*it).m_uds == UDS_LOCAL_PATH && !(*it).m_str.isEmpty()) + return engine()->load( KURL::fromPathOrURL( (*it).m_str ) ); + } + } + + //let xine handle invalid, etc, KURLS + //TODO it handles non-existant files with bad error message + return engine()->load( url ); +} + +void +MainWindow::play() +{ + switch( engine()->state() ) { + case Engine::Loaded: + engine()->play(); + break; + + case Engine::Playing: + case Engine::Paused: + engine()->pause(); + break; + + case Engine::Empty: + default: + playMedia(); + break; + } +} + +void +MainWindow::playMedia( bool show_welcome_dialog ) +{ + PlayDialog dialog( this, show_welcome_dialog ); + + switch( dialog.exec() ) { + case PlayDialog::FILE: { + const QString filter = engine()->fileFilter() + '|' + i18n("Supported Media Formats") + "\n*|" + i18n("All Files"); + const KURL url = KFileDialog::getOpenURL( ":default", filter, this, i18n("Select A File To Play") ); + open( url ); + } break; + case PlayDialog::RECENT_FILE: + open( dialog.url() ); + break; + case PlayDialog::CDDA: + open( "cdda:/1" ); + break; + case PlayDialog::VCD: + open( "vcd://" ); // one / is not enough + break; + case PlayDialog::DVD: + open( "dvd:/" ); + break; + } +} + +class FullScreenToolBarHandler : QObject +{ + KToolBar *m_toolbar; + int m_timer_id; + bool m_stay_hidden_for_a_bit; + QPoint m_home; + +public: + FullScreenToolBarHandler( KMainWindow *parent ) + : QObject( parent ) + , m_toolbar( parent->toolBar() ) + , m_timer_id( 0 ) + , m_stay_hidden_for_a_bit( false ) + { + DEBUG_BLOCK + + parent->installEventFilter( this ); + m_toolbar->installEventFilter( this ); + } + + bool eventFilter( QObject *o, QEvent *e ) + { + if (o == parent() && e->type() == QEvent::MouseMove) { + killTimer( m_timer_id ); + + QMouseEvent const * const me = (QMouseEvent*)e; + if (m_stay_hidden_for_a_bit) { + // wait for a small pause before showing the toolbar again + // usage = user removes mouse from toolbar after using it + // toolbar disappears (usage is over) but usually we show + // toolbar immediately when mouse is moved.. so we need this hack + + // HACK if user thrusts mouse to top, we assume they really want the toolbar + // back. Is hack as 80% of users have at top, but 20% at bottom, we don't cater + // for the 20% as lots more code, for now. + if (me->pos().y() < m_toolbar->height()) + goto show_toolbar; + + m_timer_id = startTimer( 100 ); + } + else { + if (m_toolbar->isHidden()) { + if (m_home.isNull()) + m_home = me->pos(); + else if ((m_home - me->pos()).manhattanLength() > 6) + // then cursor has moved far enough to trigger show toolbar +show_toolbar: + m_toolbar->show(), + m_home = QPoint(); + else + // cursor hasn't moved far enough yet + // don't reset timer below, return instead + return false; + } + + // reset the hide timer + m_timer_id = startTimer( VideoWindow::CURSOR_HIDE_TIMEOUT ); + } + } + + if (o == parent() && e->type() == QEvent::Resize) + { + //we aren't managed by mainWindow when at FullScreen + videoWindow()->move( 0, 0 ); + videoWindow()->resize( ((QWidget*)o)->size() ); + videoWindow()->lower(); + } + + if (o == m_toolbar) + switch (e->type()) { + case QEvent::Enter: + m_stay_hidden_for_a_bit = false; + killTimer( m_timer_id ); + break; + + case QEvent::Leave: + m_toolbar->hide(); + m_stay_hidden_for_a_bit = true; + killTimer( m_timer_id ); + m_timer_id = startTimer( 100 ); + break; + + default: break; + } + + return false; + } + + void timerEvent( QTimerEvent* ) + { + if (m_stay_hidden_for_a_bit) + ; + + else if (!m_toolbar->hasMouse()) + m_toolbar->hide(); + + m_stay_hidden_for_a_bit = false; + } +}; + + +void +MainWindow::fullScreenToggled( bool isFullScreen ) +{ + static FullScreenToolBarHandler *s_handler; + + DEBUG_FUNC_INFO + + if( isFullScreen ) + toolBar()->setPalette( palette() ), // due to 2px spacing in QMainWindow :( + setPaletteBackgroundColor( Qt::black ); // due to 2px spacing + else + toolBar()->unsetPalette(), + unsetPalette(); + + toolBar()->setMovingEnabled( !isFullScreen ); + toolBar()->setHidden( isFullScreen && engine()->state() == Engine::Playing ); + + reinterpret_cast(menuBar())->setHidden( isFullScreen ); + statusBar()->setHidden( isFullScreen ); + + setMouseTracking( isFullScreen ); /// @see mouseMoveEvent() + + if (isFullScreen) + s_handler = new FullScreenToolBarHandler( this ); + else + delete s_handler; + + // prevent videoWindow() moving around when mouse moves + setCentralWidget( isFullScreen ? 0 : videoWindow() ); +} + +void +MainWindow::configure() +{ + const QCString sender = this->sender()->name(); + + if( sender == "video_settings" ) + Codeine::showVideoSettingsDialog( this ); + + else if( sender == "xine_settings" ) + Codeine::showXineConfigurationDialog( this, *engine() ); +} + +void +MainWindow::streamInformation() +{ + MessageBox::information( TheStream::information(), i18n("Media Information") ); +} + +void +MainWindow::setChannels( const QStringList &channels ) +{ + DEBUG_FUNC_INFO + + //TODO -1 = auto + + QStringList::ConstIterator it = channels.begin(); + + QPopupMenu *menu = (QPopupMenu*)child( (*it).latin1() ); + menu->clear(); + + menu->insertItem( i18n("&Determine Automatically"), 1 ); + menu->insertSeparator(); + + //the id is crucial, since the slot this menu is connected to requires + //that information to set the correct channel + //NOTE we subtract 2 in xineEngine because QMenuData doesn't allow negative id + int id = 2; + ++it; + for( QStringList::ConstIterator const end = channels.end(); it != end; ++it, ++id ) + menu->insertItem( *it, id ); + + menu->insertSeparator(); + menu->insertItem( i18n("&Off"), 0 ); + + id = channels.first() == "subtitle_channels_menu" ? SubtitleChannelsMenuItemId : AudioChannelsMenuItemId; + MainWindow::menu( "settings" )->setItemEnabled( id, channels.count() > 1 ); +} + +void +MainWindow::aboutToShowMenu() +{ + QPopupMenu *menu = (QPopupMenu*)sender(); + QCString name( sender() ? sender()->name() : 0 ); + + // uncheck all items first + for( uint x = 0; x < menu->count(); ++x ) + menu->setItemChecked( menu->idAt( x ), false ); + + int id; + if( name == "subtitle_channels_menu" ) + id = TheStream::subtitleChannel() + 2; + else if( name == "audio_channels_menu" ) + id = TheStream::audioChannel() + 2; + else + id = TheStream::aspectRatio(); + + menu->setItemChecked( id, true ); +} + +void +MainWindow::dragEnterEvent( QDragEnterEvent *e ) +{ + e->accept( KURLDrag::canDecode( e ) ); +} + +void +MainWindow::dropEvent( QDropEvent *e ) +{ + KURL::List list; + KURLDrag::decode( e, list ); + + if( !list.isEmpty() ) + open( list.first() ); + else + engineMessage( i18n("Sorry, no media was found in the drop") ); +} + +void +MainWindow::keyPressEvent( QKeyEvent *e ) +{ + #define seek( step ) { \ + const int new_pos = m_positionSlider->value() step; \ + engine()->seek( new_pos > 0 ? (uint)new_pos : 0 ); \ + } + + switch( e->key() ) + { + case Qt::Key_Left: seek( -500 ); break; + case Qt::Key_Right: seek( +500 ); break; + case Key_Escape: KWin::clearState( winId(), NET::FullScreen ); + default: ; + } + + #undef seek +} + +QPopupMenu* +MainWindow::menu( const char *name ) +{ + // KXMLGUI is "really good". + return static_cast(factory()->container( name, this )); +} + + +/// Convenience class for other classes that need access to the actionCollection +KActionCollection* +actionCollection() +{ + return static_cast(kapp->mainWidget())->actionCollection(); +} + +/// Convenience class for other classes that need access to the actions +KAction* +action( const char *name ) +{ + #define QT_FATAL_ASSERT + + MainWindow *mainWindow = 0; + KActionCollection *actionCollection = 0; + KAction *action = 0; + + if( mainWindow = (MainWindow*)kapp->mainWidget() ) + if( actionCollection = mainWindow->actionCollection() ) + action = actionCollection->action( name ); + + Q_ASSERT( mainWindow ); + Q_ASSERT( actionCollection ); + Q_ASSERT( action ); + + return action; +} + +} //namespace Codeine diff --git a/src/app/mainWindow.h b/src/app/mainWindow.h new file mode 100644 index 0000000..63d8468 --- /dev/null +++ b/src/app/mainWindow.h @@ -0,0 +1,75 @@ +// (c) 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINEMAINWINDOW_H +#define CODEINEMAINWINDOW_H + +#include "codeine.h" +#include + +class KURL; +class QLabel; +class QPopupMenu; +class QSlider; + + +namespace Codeine +{ + class MainWindow : public KMainWindow + { + Q_OBJECT + + MainWindow(); + ~MainWindow(); + + friend int ::main( int, char** ); + + enum { SubtitleChannelsMenuItemId = 2000, AudioChannelsMenuItemId, AspectRatioMenuItemId }; + + public slots: + void play(); + void playMedia( bool show_welcome_dialog = false ); + + void configure(); + void streamInformation(); + void captureFrame(); + + private slots: + void engineMessage( const QString& ); + void engineStateChanged( Engine::State ); + void init(); + void showTime( int = -1 ); + void setChannels( const QStringList& ); + void aboutToShowMenu(); + void fullScreenToggled( bool ); + + private: + void setupActions(); + + bool load( const KURL& ); + bool open( const KURL& ); + + QPopupMenu *menu( const char *name ); + + virtual void timerEvent( QTimerEvent* ); + virtual void dragEnterEvent( QDragEnterEvent* ); + virtual void dropEvent( QDropEvent* ); + virtual void keyPressEvent( QKeyEvent* ); + + virtual void saveProperties( KConfig* ); + virtual void readProperties( KConfig* ); + + virtual bool queryExit(); + + QSlider *m_positionSlider; + QLabel *m_timeLabel; + QLabel *m_titleLabel; + QWidget *m_analyzer; + + //undefined + MainWindow( const MainWindow& ); + MainWindow &operator=( const MainWindow& ); + }; +} + +#endif diff --git a/src/app/playDialog.cpp b/src/app/playDialog.cpp new file mode 100644 index 0000000..50a9ca2 --- /dev/null +++ b/src/app/playDialog.cpp @@ -0,0 +1,114 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "config.h" +#include "listView.cpp" +#include +#include +#include +#include +#include +#include +#include "playDialog.h" +#include "mxcl.library.h" +#include +#include +#include +#include + +QString i18n( const char *text ); + + +namespace Codeine { + + +PlayDialog::PlayDialog( QWidget *parent, bool be_welcome_dialog ) + : QDialog( parent ) +{ + setCaption( kapp->makeStdCaption( i18n("Play Media") ) ); + + QSignalMapper *mapper = new QSignalMapper( this ); + QWidget *o, *closeButton = new KPushButton( KStdGuiItem::close(), this ); + QBoxLayout *hbox, *vbox = new QVBoxLayout( this, 15, 20 ); + + vbox->addWidget( new QLabel( i18n( "What media would you like to play?" ), this ) ); + + QGridLayout *grid = new QGridLayout( vbox, 1, 3, 20 ); + + //TODO use the kguiItems from the actions + mapper->setMapping( o = new KPushButton( KGuiItem( i18n("Play File..."), "fileopen" ), this ), FILE ); + connect( o, SIGNAL(clicked()), mapper, SLOT(map()) ); + grid->QLayout::add( o ); + + mapper->setMapping( o = new KPushButton( KGuiItem( i18n("Play VCD"), "cdaudio_unmount" ), this ), VCD ); + connect( o, SIGNAL(clicked()), mapper, SLOT(map()) ); + grid->QLayout::add( o ); + + mapper->setMapping( o = new KPushButton( KGuiItem( i18n("Play DVD"), "dvd_unmount" ), this ), DVD ); + connect( o, SIGNAL(clicked()), mapper, SLOT(map()) ); + grid->QLayout::add( o ); + + mapper->setMapping( closeButton, QDialog::Rejected ); + connect( closeButton, SIGNAL(clicked()), mapper, SLOT(map()) ); + + createRecentFileWidget( vbox ); + + hbox = new QHBoxLayout( vbox ); + hbox->addItem( new QSpacerItem( 10, 10, QSizePolicy::Expanding ) ); + + if( be_welcome_dialog ) { + QWidget *w = new KPushButton( KStdGuiItem::quit(), this ); + hbox->addWidget( w ); + connect( w, SIGNAL(clicked()), kapp, SLOT(quit()) ); + } + + hbox->addWidget( closeButton ); + + connect( mapper, SIGNAL(mapped( int )), SLOT(done( int )) ); +} + +void +PlayDialog::createRecentFileWidget( QBoxLayout *layout ) +{ + KListView *lv; + lv = new Codeine::ListView( this ); + lv->setColumnText( 1, i18n("Recently Played Media") ); + + const QStringList list1 = Codeine::config( "General" )->readPathListEntry( "Recent Urls" ); + KURL::List urls; + + foreach( list1 ) + urls += *it; + + for( KURL::List::Iterator it = urls.begin(), end = urls.end(); it != end; ) { + if( urls.contains( *it ) > 1 ) + //remove duplicates + it = urls.remove( it ); + else if( (*it).protocol() == "file" && !QFile::exists( (*it).path() ) ) + //remove stale entries + it = urls.remove( it ); + else + ++it; + } + + for( KURL::List::ConstIterator it = urls.begin(), end = urls.end(); it != end; ++it ) { + const QString fileName = (*it).fileName(); + new KListViewItem( lv, 0, (*it).url(), fileName.isEmpty() ? (*it).prettyURL() : fileName ); + } + + if( lv->childCount() ) { + layout->addWidget( lv, 1 ); + connect( lv, SIGNAL(executed( QListViewItem* )), SLOT(done( QListViewItem* )) ); + } + else + delete lv; +} + +void +PlayDialog::done( QListViewItem *item ) +{ + m_url = item->text( 0 ); + QDialog::done( RECENT_FILE ); +} + +} diff --git a/src/app/playDialog.h b/src/app/playDialog.h new file mode 100644 index 0000000..020f9f1 --- /dev/null +++ b/src/app/playDialog.h @@ -0,0 +1,36 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINEPLAYDIALOG_H +#define CODEINEPLAYDIALOG_H + +#include +#include + +class KListView; +class QBoxLayout; +class QListViewItem; + +namespace Codeine +{ + class PlayDialog : public QDialog + { + Q_OBJECT + public: + PlayDialog( QWidget*, bool show_welcome_dialog = false ); + + const KURL &url() const { return m_url; } + + enum DialogCode { FILE = QDialog::Accepted + 2, VCD, CDDA, DVD, RECENT_FILE }; + + private slots: + void done( QListViewItem* ); + + private: + void createRecentFileWidget( QBoxLayout* ); + + KURL m_url; + }; +} + +#endif diff --git a/src/app/playlistFile.cpp b/src/app/playlistFile.cpp new file mode 100644 index 0000000..19acd30 --- /dev/null +++ b/src/app/playlistFile.cpp @@ -0,0 +1,123 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + + +//TODO error messages that vary depending on if the file is remote or not + + +#include "codeine.h" +#include "debug.h" +#include +#include "playlistFile.h" +#include +#include +#include + + +PlaylistFile::PlaylistFile( const KURL &url ) + : m_url( url ) + , m_isRemoteFile( !url.isLocalFile() ) + , m_isValid( false ) +{ + mxcl::WaitCursor allocateOnStack; + + QString &path = m_path = url.path(); + + if( path.endsWith( ".pls", false ) ) + m_type = PLS; else + if( path.endsWith( ".m3u", false ) ) + m_type = M3U; + else { + m_type = Unknown; + m_error = i18n( "The file is not a playlist" ); + return; + } + + if( m_isRemoteFile ) { + path = QString(); + if( !KIO::NetAccess::download( url, path, Codeine::mainWindow() ) ) { + m_error = i18n( "Codeine could not download the remote playlist: %1" ).arg( url.prettyURL() ); + return; + } + } + + QFile file( path ); + if( file.open( IO_ReadOnly ) ) { + QTextStream stream( &file ); + switch( m_type ) { + case M3U: parseM3uFile( stream ); break; + case PLS: parsePlsFile( stream ); break; + default: ; + } + + if( m_contents.isEmpty() ) + m_error = i18n( "The playlist, '%1', could not be interpreted. Perhaps it is empty?" ).arg( path ), + m_isValid = false; + } + else + m_error = i18n( "Codeine could not open the file: %1" ).arg( path ); +} + + +PlaylistFile::~PlaylistFile() +{ + if( m_isRemoteFile ) + KIO::NetAccess::removeTempFile( m_path ); +} + + +void +PlaylistFile::parsePlsFile( QTextStream &stream ) +{ + DEBUG_BLOCK + + for( QString line = stream.readLine(); !line.isNull(); ) + { + if( line.startsWith( "File" ) ) { + const KURL url = line.section( '=', -1 ); + const QString title = stream.readLine().section( '=', -1 ); + + debug() << url << endl << title << endl; + + m_contents += url; + m_isValid = true; + + return; //TODO continue for all urls + } + line = stream.readLine(); + } +} + + +void +PlaylistFile::parseM3uFile( QTextStream &stream ) +{ + DEBUG_BLOCK + + for( QString line; !stream.atEnd(); ) + { + line = stream.readLine(); + + if( line.startsWith( "#EXTINF", false ) ) + continue; + + else if( !line.startsWith( "#" ) && !line.isEmpty() ) + { + KURL url; + + // KURL::isRelativeURL() expects absolute URLs to start with a protocol, so prepend it if missing + if( line.startsWith( "/" ) ) + line.prepend( "file://" ); + + if( KURL::isRelativeURL( line ) ) + url.setPath( m_url.directory() + line ); + else + url = KURL::fromPathOrURL( line ); + + m_contents += url; + m_isValid = true; + + return; + } + } +} diff --git a/src/app/playlistFile.h b/src/app/playlistFile.h new file mode 100644 index 0000000..0302a85 --- /dev/null +++ b/src/app/playlistFile.h @@ -0,0 +1,36 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_PLAYLIST_FILE_H +#define CODEINE_PLAYLIST_FILE_H + +#include + +class PlaylistFile +{ +public: + PlaylistFile( const KURL &url ); + ~PlaylistFile(); + + enum FileFormat { M3U, PLS, Unknown, NotPlaylistFile = Unknown }; + + bool isPlaylist() const { return m_type != Unknown; } + bool isValid() const { return m_isValid; } + KURL firstUrl() const { return m_contents.isEmpty() ? KURL() : m_contents.first(); } + QString error() const { return m_error; } + +private: + /// both only return first url currently + void parsePlsFile( QTextStream& ); + void parseM3uFile( QTextStream& ); + + KURL m_url; + bool m_isRemoteFile; + bool m_isValid; + QString m_error; + FileFormat m_type; + QString m_path; + KURL::List m_contents; +}; + +#endif diff --git a/src/app/slider.cpp b/src/app/slider.cpp new file mode 100644 index 0000000..89b5ced --- /dev/null +++ b/src/app/slider.cpp @@ -0,0 +1,145 @@ +// (c) 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "debug.h" +#include "slider.h" +#include +#include +#include +#include + +#include +#include "xineEngine.h" + +using Codeine::Slider; + + +Slider *Slider::s_instance = 0; + + +Slider::Slider( QWidget *parent, uint max ) + : QSlider( Qt::Horizontal, parent ) + , m_sliding( false ) + , m_outside( false ) + , m_prevValue( 0 ) +{ + s_instance = this; + + setRange( 0, max ); + setFocusPolicy( NoFocus ); + setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding ); +} + +void +Slider::wheelEvent( QWheelEvent *e ) +{ + //if you use this class elsewhere, NOTE this is Codeine specific + e->ignore(); //pass to VideoWindow +} + +void +Slider::mouseMoveEvent( QMouseEvent *e ) +{ + if( m_sliding ) + { + //feels better, but using set value of 20 is bad of course + QRect rect = this->rect(); + rect.addCoords( -20, -20, 20, 20 ); + + if( !rect.contains( e->pos() ) ) { + if( !m_outside ) + QSlider::setValue( m_prevValue ); + m_outside = true; + } else { + m_outside = false; + + QSlider::setValue( + QRangeControl::valueFromPosition( + e->pos().x() - sliderRect().width()/2, + width() - sliderRect().width() ) ); + + emit sliderMoved( value() ); + } + } + else + QSlider::mouseMoveEvent( e ); +} + +void +Slider::mousePressEvent( QMouseEvent *e ) +{ + m_sliding = true; + m_prevValue = QSlider::value(); + + if( !sliderRect().contains( e->pos() ) ) + mouseMoveEvent( e ); +} + +void +Slider::mouseReleaseEvent( QMouseEvent* ) +{ + if( !m_outside && QSlider::value() != m_prevValue ) + emit sliderReleased( value() ); + + m_sliding = false; + m_outside = false; +} + +static inline QString timeAsString( const int s ) +{ + #define zeroPad( n ) n < 10 ? QString("0%1").arg( n ) : QString::number( n ) + using Codeine::engine; + + const int m = s / 60; + const int h = m / 60; + + QString time; + time.prepend( zeroPad( s % 60 ) ); //seconds + time.prepend( ':' ); + time.prepend( zeroPad( m % 60 ) ); //minutes + time.prepend( ':' ); + time.prepend( QString::number( h ) ); //hours + + return time; +} + +void +Slider::setValue( int newValue ) +{ + static QLabel *w1 = 0; + static QLabel *w2 = 0; + + if (!w1) { + w1 = new QLabel( this ); + w1->setPalette( QToolTip::palette() ); + w1->setFrameStyle( QFrame::Plain | QFrame::Box ); + + w2 = new QLabel( this ); + w2->setPalette( QToolTip::palette() ); + w2->setFrameStyle( QFrame::Plain | QFrame::Box ); + } + + //TODO stupidly inefficeint! :) + w1->setShown( mainWindow()->isFullScreen() ); + w2->setShown( mainWindow()->isFullScreen() ); + + + //don't adjust the slider while the user is dragging it! + + if( !m_sliding || m_outside ) { + const int l = engine()->length() / 1000; + const int left = int(l * (newValue / 65535.0)); + const int right = l - left; + + QSlider::setValue( newValue ); + w1->move( 0, height() - w1->height() - 1 ); + w1->setText( timeAsString( left ) + ' ' ); + w1->adjustSize(); + + w2->move( width() - w2->width(), height() - w1->height() - 1 ); + w2->setText( timeAsString( right ) + ' ' ); + w2->adjustSize(); + } + else + m_prevValue = newValue; +} diff --git a/src/app/slider.h b/src/app/slider.h new file mode 100644 index 0000000..7e06b6b --- /dev/null +++ b/src/app/slider.h @@ -0,0 +1,52 @@ +// (c) 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINESLIDER_H +#define CODEINESLIDER_H + +#include + +namespace Codeine +{ + class Slider : public QSlider + { + Q_OBJECT + + public: + static Slider *instance() { return s_instance; } + + public: + Slider( QWidget*, uint max = 0 ); + + virtual void setValue( int ); + + signals: + //we emit this when the user has specifically changed the slider + //so connect to it if valueChanged() is too generic + //Qt also emits valueChanged( int ) + void sliderReleased( uint ); + + protected: + virtual void wheelEvent( QWheelEvent* ); + virtual void mouseMoveEvent( QMouseEvent* ); + virtual void mouseReleaseEvent( QMouseEvent* ); + virtual void mousePressEvent( QMouseEvent* ); + virtual void keyPressEvent( QKeyEvent *e ) { e->ignore(); } //so that MainWindow gets the keypress + + virtual QSize sizeHint() const { return QSlider::sizeHint() + QSize( 0, 6 ); } + virtual QSize minimumSizeHint() const { return sizeHint(); } + + bool m_sliding; + + private: + static Slider *s_instance; + + bool m_outside; + int m_prevValue; + + Slider( const Slider& ); //undefined + Slider &operator=( const Slider& ); //undefined + }; +} + +#endif diff --git a/src/app/stateChange.cpp b/src/app/stateChange.cpp new file mode 100644 index 0000000..be15aeb --- /dev/null +++ b/src/app/stateChange.cpp @@ -0,0 +1,195 @@ +// Copyright 2004 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "actions.h" +#include "adjustSizeButton.h" +#include "debug.h" +#include "mainWindow.h" +#include +#include +#include "mxcl.library.h" +#include +#include +#include +#include +#include +#include "theStream.h" +#include "videoSettings.h" //FIXME unfortunate +#include "xineEngine.h" + + +//TODO do in Sconstruct +#define QT_FATAL_ASSERT + + +//TODO make the XineEngine into xine::Stream and then make singleton and add functions like Stream::hasVideo() etc. +//TODO make convenience function to get fullscreen state + + +namespace Codeine { + + +void +MainWindow::engineStateChanged( Engine::State state ) +{ + Q_ASSERT( state != Engine::Uninitialised ); + + KURL const &url = TheStream::url(); + bool const isFullScreen = toggleAction("fullscreen")->isChecked(); + QWidget *const toolbar = reinterpret_cast(toolBar()); + + Debug::Block block( state == Engine::Empty + ? "State: Empty" : state == Engine::Loaded + ? "State: Loaded" : state == Engine::Playing + ? "State: Playing" : state == Engine::Paused + ? "State: Paused" : state == Engine::TrackEnded + ? "State: TrackEnded" : "State: Unknown" ); + + + /// update actions + { + using namespace Engine; + + #define enableIf( name, criteria ) action( name )->setEnabled( state & criteria ); + enableIf( "stop", (Playing | Paused) ); + enableIf( "fullscreen", (Playing | Paused) ); + enableIf( "reset_zoom", ~Empty && !isFullScreen ); + enableIf( "information", ~Empty ); + enableIf( "video_settings", (Playing | Paused) ); + enableIf( "volume", (Playing | Paused) ); + #undef enableIf + + toggleAction( "play" )->setChecked( state == Playing ); + + //FIXME bad design to do this way + QSlider *volume = (QSlider*)toolBar()->child( "volume" ); + if (volume) + volume->setValue( engine()->volume() ); + } + + + /// update VideoSettingsDialog instance + VideoSettingsDialog::stateChanged( this, state ); + + + /// update menus + { + using namespace Engine; + + // the toolbar play button is always enabled, but the menu item + // is disabled if we are empty, this looks more sensible + QPopupMenu * const file_menu = menu( "file" ); + QPopupMenu * const settings_menu = menu( "settings" ); + const int play_id = file_menu->idAt( 2 ); + file_menu->setItemEnabled( play_id, state != Empty ); + + // menus are clearer when handled differently to toolbars + // KDE has a shit special action for this, but it stupidly changes + // the toolbar icon too. + // TODO do this from the playAction since we do it in context menu too + const KGuiItem item = (state == Playing) ? KGuiItem( i18n("&Pause"), "player_pause" ) : KGuiItem( i18n("&Play"), "player_play" ); + file_menu->changeItem( play_id, item.iconSet(), item.text() ); + file_menu->setItemChecked( play_id, false ); + + settings_menu->setItemEnabled( AspectRatioMenuItemId, state & (Playing | Paused) && TheStream::hasVideo() ); + + // set correct aspect ratio + if( state == Loaded ) + static_cast(child( "aspect_ratio_menu" ))->setItemChecked( TheStream::aspectRatio(), true ); + } + + + /// update statusBar + { + using namespace Engine; + m_analyzer->setShown( state & (Playing | Paused) && TheStream::hasAudio() ); + m_timeLabel->setShown( state & (Playing | Paused) ); + } + + + /// update position slider + switch( state ) + { + case Engine::Empty: + m_positionSlider->setEnabled( false ); + break; + case Engine::Loaded: + case Engine::TrackEnded: + m_positionSlider->setValue( 0 ); + // NO BREAK! + case Engine::Playing: + case Engine::Paused: + m_positionSlider->setEnabled( TheStream::canSeek() ); + break; + } + + + /// update recent files list if necessary + if( state == Engine::Loaded ) { + // update recently played list + + #ifndef NO_SKIP_PR0N + // ;-) + const QString url_string = url.url(); + if( !(url_string.contains( "porn", false ) || url_string.contains( "pr0n", false )) ) + #endif + if( url.protocol() != "dvd" && url.protocol() != "vcd" ) { + KConfig *config = Codeine::config( "General" ); + const QString prettyUrl = url.prettyURL(); + + QStringList urls = config->readPathListEntry( "Recent Urls" ); + urls.remove( prettyUrl ); + config->writePathEntry( "Recent Urls", urls << prettyUrl ); + } + + if( TheStream::hasVideo() && !isFullScreen ) + new AdjustSizeButton( reinterpret_cast(videoWindow()) ); + } + + + /// set titles + switch( state ) + { + case Engine::Empty: + m_titleLabel->setText( i18n("No media loaded") ); + break; + case Engine::Paused: + m_titleLabel->setText( i18n("Paused") ); + break; + case Engine::Loaded: + case Engine::Playing: + case Engine::TrackEnded: + m_titleLabel->setText( TheStream::prettyTitle() ); + break; + } + + + /// set toolbar states + QWidget *dvd_button = (QWidget*)toolBar()->child( "toolbutton_toggle_dvd_menu" ); + if (dvd_button) + dvd_button->setShown( state != Engine::Empty && url.protocol() == "dvd" ); + + if( isFullScreen && !toolbar->hasMouse() ) { + switch( state ) { + case Engine::TrackEnded: + toolbar->show(); + + if( videoWindow()->isActiveWindow() ) { + //FIXME dual-screen this seems to still show + QContextMenuEvent e( QContextMenuEvent::Other, QPoint(), Qt::MetaButton ); + QApplication::sendEvent( videoWindow(), &e ); + } + break; + case Engine::Empty: + case Engine::Loaded: + case Engine::Paused: + toolBar()->show(); + break; + case Engine::Playing: + toolBar()->hide(); + break; + } + } +} + +} diff --git a/src/app/theStream.cpp b/src/app/theStream.cpp new file mode 100644 index 0000000..5d60d76 --- /dev/null +++ b/src/app/theStream.cpp @@ -0,0 +1,144 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include +#include "mxcl.library.h" +#include "theStream.h" +#include +#include "xineEngine.h" + +namespace Codeine +{ + #define e VideoWindow::s_instance + + KConfig* + TheStream::profile() + { +//TODO a unique id for discs, and then even to also record chapters etc. +// if( url().protocol() == "dvd" ) +// return Codeine::config( QString( "dvd:/" ) + prettyTitle() ); +// else + return Codeine::config( url().prettyURL() ); + } + + const KURL& + TheStream::url() + { return e->m_url; } + + bool + TheStream::canSeek() + //FIXME! + { return e->m_url.protocol() != "http"; } + + bool + TheStream::hasAudio() + { return xine_get_stream_info( e->m_stream, XINE_STREAM_INFO_HAS_AUDIO ); } + + bool + TheStream::hasVideo() + { return xine_get_stream_info( e->m_stream, XINE_STREAM_INFO_HAS_VIDEO ); } + + QSize + TheStream::defaultVideoSize() + { + return !e->m_stream + ? QSize() + : QSize( + xine_get_stream_info( e->m_stream, XINE_STREAM_INFO_VIDEO_WIDTH ), + xine_get_stream_info( e->m_stream, XINE_STREAM_INFO_VIDEO_HEIGHT ) ); + } + + int TheStream::aspectRatio() + { return xine_get_param( e->m_stream, XINE_PARAM_VO_ASPECT_RATIO ); } + + int TheStream::subtitleChannel() + { return xine_get_param( e->m_stream, XINE_PARAM_SPU_CHANNEL ); } + + int TheStream::audioChannel() + { return xine_get_param( e->m_stream, XINE_PARAM_AUDIO_CHANNEL_LOGICAL ); } + + QString + TheStream::prettyTitle() + { + const KURL &url = e->m_url; + const QString artist = QString::fromUtf8( xine_get_meta_info( e->m_stream, XINE_META_INFO_ARTIST ) ); + const QString title = QString::fromUtf8( xine_get_meta_info( e->m_stream, XINE_META_INFO_TITLE ) ); + + if (hasVideo() && !title.isEmpty()) + return title; + else if (!title.isEmpty() && !artist.isEmpty()) + return artist + " - " + title; + else if (url.protocol() != "http" && !url.fileName().isEmpty()) { + const QString n = url.fileName(); + return KURL::decode_string( n.left( n.findRev( '.' ) ).replace( '_', ' ' ) ); } + else + return url.prettyURL(); + } + + + static inline QString + entryHelper( const QString &plate, const QString &s1, const QString &s2 ) + { + return s2.isEmpty() ? s2 : plate.arg( s1 ).arg( s2 ); + } + + static inline QString + sectionHelper( const QString §ionTitle, const QStringList &entries ) + { + QString s; + + foreach( entries ) + if( !(*it).isEmpty() ) + s += *it; + + return s.isEmpty() ? s : "

" + sectionTitle + "

" + s; + } + + QString + TheStream::information() + { + #define meta( x ) xine_get_meta_info( e->m_stream, x ) + #define info( x, y ) x.arg( xine_get_stream_info( e->m_stream, y ) ) + #define simple( x ) QString::number( xine_get_stream_info( e->m_stream, x ) ) + + const QString plate = "

%1: %2

"; + QString s; + + s += sectionHelper( i18n("Metadata"), + QStringList() + << entryHelper( plate, i18n("Title"), meta( XINE_META_INFO_TITLE ) ) + << entryHelper( plate, i18n("Comment"), meta( XINE_META_INFO_COMMENT ) ) + << entryHelper( plate, i18n("Artist"), meta( XINE_META_INFO_ARTIST ) ) + << entryHelper( plate, i18n("Genre"), meta( XINE_META_INFO_GENRE ) ) + << entryHelper( plate, i18n("Album"), meta( XINE_META_INFO_ALBUM ) ) + << entryHelper( plate, i18n("Year"), meta( XINE_META_INFO_YEAR ) ) ); + + s += sectionHelper( i18n("Audio Properties"), + QStringList() + << entryHelper( plate, i18n("Bitrate"), info( i18n("%1 bps"), XINE_STREAM_INFO_AUDIO_BITRATE ) ) + << entryHelper( plate, i18n("Sample-rate"), info( i18n("%1 Hz"), XINE_STREAM_INFO_AUDIO_SAMPLERATE ) ) ); + + s += sectionHelper( i18n("Technical Information"), + QStringList() + << entryHelper( plate, i18n("Video Codec"), meta( XINE_META_INFO_VIDEOCODEC ) ) + << entryHelper( plate, i18n("Audio Codec"), meta( XINE_META_INFO_AUDIOCODEC ) ) + << entryHelper( plate, i18n("System Layer"), meta( XINE_META_INFO_SYSTEMLAYER ) ) + << entryHelper( plate, i18n("Input Plugin"), meta( XINE_META_INFO_INPUT_PLUGIN )) + << entryHelper( plate, i18n("CDINDEX_DISCID"), meta( XINE_META_INFO_CDINDEX_DISCID ) ) ); + + QStringList texts; + texts << "BITRATE" << "SEEKABLE" << "VIDEO_WIDTH" << "VIDEO_HEIGHT" << "VIDEO_RATIO" << "VIDEO_CHANNELS" << "VIDEO_STREAMS" << "VIDEO_BITRATE" << "VIDEO_FOURCC" << "VIDEO_HANDLED" << "FRAME_DURATION" << "AUDIO_CHANNELS" << "AUDIO_BITS" << "-AUDIO_SAMPLERATE" << "-AUDIO_BITRATE" << "AUDIO_FOURCC" << "AUDIO_HANDLED" << "HAS_CHAPTERS" << "HAS_VIDEO" << "HAS_AUDIO" << "-IGNORE_VIDEO" << "-IGNORE_AUDIO" << "-IGNORE_SPU" << "VIDEO_HAS_STILL" << "MAX_AUDIO_CHANNEL" << "MAX_SPU_CHANNEL" << "AUDIO_MODE" << "SKIPPED_FRAMES" << "DISCARDED_FRAMES"; + + s += "

Other

"; + for( uint x = 0; x <= 28; ++x ) + s += entryHelper( plate, texts[x], simple( x ) ); + + #undef meta + #undef info + #undef simple + + return s; + } + + #undef e +} diff --git a/src/app/theStream.h b/src/app/theStream.h new file mode 100644 index 0000000..0ffe64f --- /dev/null +++ b/src/app/theStream.h @@ -0,0 +1,50 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_THESTREAM_H +#define CODEINE_THESTREAM_H + +#include "config.h" // needed for inline functions +#include // larger :( but no macros at least +#include // small header +#include // small header + +/// for purely static classes +#define CODEINE_NO_EXPORT( T ) \ + T(); \ + ~T(); \ + T( const T& ); \ + T &operator=( const T& ); \ + bool operator==( const T& ); \ + bool operator!=( const T& ); + +namespace Codeine +{ + class TheStream + { + CODEINE_NO_EXPORT( TheStream ) + + public: + static const KURL &url(); + + static bool canSeek(); + static bool hasAudio(); + static bool hasVideo(); + + static QSize defaultVideoSize(); + + static int aspectRatio(); + static int subtitleChannel(); + static int audioChannel(); + + static QString prettyTitle(); + static QString information(); + + static inline bool hasProfile() + { return KGlobal::config()->hasGroup( url().prettyURL() ); } + + static KConfig *profile(); + }; +} + +#endif diff --git a/src/app/videoSettings.cpp b/src/app/videoSettings.cpp new file mode 100644 index 0000000..945e4d3 --- /dev/null +++ b/src/app/videoSettings.cpp @@ -0,0 +1,135 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include +#include "mxcl.library.h" +#include +#include +#include +#include "videoSettings.h" +#include +#include "xineEngine.h" + +extern "C" +{ + // #include is just dangerous! Here, there is a macro for Below that conflicts + // with QSlider::Below. Stupid X11 people. + typedef unsigned long XID; + typedef XID Window; + extern int XSetTransientForHint( Display*, Window, Window ); +} + + +//TODO update from engine when new video is played +//TODO show a warning that when paused the changes aren't updated to the display, show an unpause button too + + +class SnapSlider : public QSlider +{ + int m_offset; + +public: + SnapSlider( const int value, QWidget *parent, const char *name ) + : QSlider( (65536/4)-1, (3*(65536/4))-1, 1000, value, Qt::Horizontal, parent, name ) + , m_offset( 0 ) + { + setTickmarks( QSlider::Below ); + setTickInterval( 65536 / 4 ); + setMinimumWidth( fontMetrics().width( name ) * 3 ); + connect( this, SIGNAL(valueChanged( int )), Codeine::engine(), SLOT(setStreamParameter( int )) ); + } + + virtual void mousePressEvent( QMouseEvent *e ) + { + m_offset = e->pos().x() - (sliderStart() + (sliderRect().width()/2)); + QSlider::mousePressEvent( e ); + } + + virtual void mouseMoveEvent( QMouseEvent *e ) + { + const int MIDDLE = width() / 2; + const int x = e->pos().x() - m_offset; + const int F = sliderRect().width() / 2; + + if( x > MIDDLE - F && x < MIDDLE + F ) { + QMouseEvent e2( e->type(), QPoint( MIDDLE + m_offset, e->pos().y() ), e->button(), e->state() ); + QSlider::mouseMoveEvent( &e2 ); + QRangeControl::setValue( 65536 / 2 - 1 ); // to ensure we are absolutely exact + } + else + QSlider::mouseMoveEvent( e ); + } +}; + + +Codeine::VideoSettingsDialog::VideoSettingsDialog( QWidget *parent ) + : KDialog( parent, "video_settings_dialog", false, WType_TopLevel | WDestructiveClose ) +{ + XSetTransientForHint( x11Display(), winId(), parent->winId() ); + KWin::setType( winId(), NET::Utility ); + KWin::setState( winId(), NET::SkipTaskbar ); + + QFrame *frame = new QFrame( this ); + (new QVBoxLayout( this, 10 ))->addWidget( frame ); + frame->setFrameStyle( QFrame::StyledPanel | QFrame::Sunken ); + frame->setPaletteBackgroundColor( backgroundColor().dark( 102 ) ); + + QGridLayout *grid = new QGridLayout( frame, 4, 2, 15, 10 ); + grid->setAutoAdd( true ); + + #define makeSlider( PARAM, name ) \ + new QLabel( name, frame ); \ + new SnapSlider( xine_get_param( *Codeine::engine(), PARAM ), frame, name ); + + makeSlider( XINE_PARAM_VO_BRIGHTNESS, "brightness" ); + makeSlider( XINE_PARAM_VO_CONTRAST, "contrast" ); + makeSlider( XINE_PARAM_VO_SATURATION, "saturation" ); + makeSlider( XINE_PARAM_VO_HUE, "hue" ); + + #undef makeSlider + + setCaption( i18n("Video Settings") ); + setMaximumSize( sizeHint().width() * 5, sizeHint().height() ); + + KDialog::show(); +} + +void +Codeine::VideoSettingsDialog::stateChanged( QWidget *parent, Engine::State state ) //static +{ + QWidget *me = (QWidget*)parent->child( "video_settings_dialog" ); + + if( !me ) + return; + + switch( state ) + { + case Engine::Playing: + case Engine::Paused: + me->setEnabled( true ); + break; + + case Engine::Loaded: + #define update( param, name ) static_cast(me->child( name ))->setValue( xine_get_param( *Codeine::engine(), param ) ); + update( XINE_PARAM_VO_BRIGHTNESS, "brightness" ); + update( XINE_PARAM_VO_CONTRAST, "contrast" ); + update( XINE_PARAM_VO_SATURATION, "saturation" ); + update( XINE_PARAM_VO_HUE, "hue" ); + #undef update + + default: + me->setEnabled( false ); + break; + } +} + +namespace Codeine +{ + void showVideoSettingsDialog( QWidget *parent ) + { + // ensure that the dialog is shown by deleting the old one + delete parent->child( "video_settings_dialog" ); + + new VideoSettingsDialog( parent ); + } +} diff --git a/src/app/videoSettings.h b/src/app/videoSettings.h new file mode 100644 index 0000000..20e01ff --- /dev/null +++ b/src/app/videoSettings.h @@ -0,0 +1,26 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_VIDEO_SETTINGS_H +#define CODEINE_VIDEO_SETTINGS_H + +#include "codeine.h" +#include + + +namespace Codeine +{ + class VideoSettingsDialog : public KDialog + { + VideoSettingsDialog(); //disable + VideoSettingsDialog( const VideoSettingsDialog& ); //disable + VideoSettingsDialog &operator=( const VideoSettingsDialog& ); //disable + + public: + VideoSettingsDialog( QWidget *parent ); + + static void stateChanged( QWidget *parent, Engine::State ); + }; +} + +#endif diff --git a/src/app/videoWindow.cpp b/src/app/videoWindow.cpp new file mode 100644 index 0000000..00f2542 --- /dev/null +++ b/src/app/videoWindow.cpp @@ -0,0 +1,380 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#define CODEINE_DEBUG_PREFIX "VideoWindow" + +#include "actions.h" +#include //std::log10 +#include +#include "debug.h" +#include //::makeStandardCaption +#include +#include +#include +#include +#include "mxcl.library.h" +#include +#include +#include "slider.h" +#include "theStream.h" +#include +#include +#include "xineEngine.h" + + +namespace Codeine +{ + namespace X + { + // we get thread locks if we don't cache these values + // (I don't know which ones exactly) + Display *d; + int s, w; + } + + +void +VideoWindow::initVideo() +{ + X::d = XOpenDisplay( std::getenv("DISPLAY") ); + X::s = DefaultScreen( X::d ); + X::w = winId(); + + XLockDisplay( X::d ); + XSelectInput( X::d, X::w, ExposureMask ); + + { + using X::d; using X::s; + + //these are Xlib macros + double w = DisplayWidth( d, s ) * 1000 / DisplayWidthMM( d, s ); + double h = DisplayHeight( d, s ) * 1000 / DisplayHeightMM( d, s ); + + m_displayRatio = w / h; + } + + connect( &m_timer, SIGNAL(timeout()), SLOT(hideCursor()) ); + + XUnlockDisplay( X::d ); +} + +void +VideoWindow::cleanUpVideo() +{ + XCloseDisplay( X::d ); +} + +void* +VideoWindow::x11Visual() const +{ + DEBUG_FUNC_INFO + + x11_visual_t* visual = new x11_visual_t; + + visual->display = X::d; + visual->screen = X::s; + visual->d = winId();//X::w; + visual->dest_size_cb = &VideoWindow::destSizeCallBack; + visual->frame_output_cb = &VideoWindow::frameOutputCallBack; + visual->user_data = (void*)this; + + return visual; +} + +void +VideoWindow::destSizeCallBack( + void* p, int /*video_width*/, int /*video_height*/, + double /*video_aspect*/, int* dest_width, + int* dest_height, double* dest_aspect ) +{ + if( !p ) + return; + + #define vw static_cast(p) + + *dest_width = vw->width(); + *dest_height = vw->height(); + *dest_aspect = vw->m_displayRatio; +} + +void +VideoWindow::frameOutputCallBack( + void* p, int video_width, int video_height, double video_aspect, + int* dest_x, int* dest_y, int* dest_width, int* dest_height, + double* dest_aspect, int* win_x, int* win_y ) +{ + if( !p ) + return; + + *dest_x = 0; + *dest_y = 0 ; + *dest_width = vw->width(); + *dest_height = vw->height(); + *win_x = vw->x(); + *win_y = vw->y(); + *dest_aspect = vw->m_displayRatio; + + // correct size with video aspect + // TODO what's this about? + if( video_aspect >= vw->m_displayRatio ) + video_width = (int) ( (double) (video_width * video_aspect / vw->m_displayRatio + 0.5) ); + else + video_height = (int) ( (double) (video_height * vw->m_displayRatio / video_aspect) + 0.5); + + #undef vw +} + +void +VideoWindow::contextMenuEvent( QContextMenuEvent *e ) +{ + e->accept(); + + KPopupMenu popup; + + if( state() == Engine::Playing ) + popup.insertItem( SmallIconSet("player_pause"), i18n("Pause"), 1 ); + else + action( "play" )->plug( &popup ); + + popup.insertSeparator(); + + if( TheStream::url().protocol() == "dvd" ) + action( "toggle_dvd_menu" )->plug( &popup ), + popup.insertSeparator(); + if( !((KToggleAction*)actionCollection()->action( "fullscreen" ))->isChecked() ) + action( "reset_zoom" )->plug( &popup ); + action( "capture_frame" )->plug( &popup ); + popup.insertSeparator(); + action( "video_settings" )->plug( &popup ); + popup.insertSeparator(); + action( "fullscreen" )->plug( &popup ); + //show zoom information? + + if( e->state() & Qt::MetaButton ) { //only on track end, or for special users + popup.insertSeparator(); + action( "file_quit" )->plug( &popup ); + } + + if( popup.exec( e->globalPos() ) == 1 && state() == Engine::Playing ) + // we check we are still paused as the menu generates a modal event loop + // so anything might have happened in the meantime. + pause(); +} + +bool +VideoWindow::event( QEvent *e ) +{ + //TODO it would perhaps make things more responsive to + // deactivate mouse tracking and use the x11Event() function to transfer mouse move events? + // perhaps even better would be a x11 implementation + + switch( e->type() ) + { + case QEvent::DragEnter: + case QEvent::Drop: + //FIXME why don't we just ignore the event? It should propogate down + return QApplication::sendEvent( qApp->mainWidget(), e ); + + case QEvent::Resize: + if( !TheStream::url().isEmpty() ) { + const QSize defaultSize = TheStream::defaultVideoSize(); + const bool notDefaultSize = width() != defaultSize.width() && height() != defaultSize.height(); + + Codeine::action( "reset_zoom" )->setEnabled( notDefaultSize ); + + //showOSD( i18n("Scale: %1%").arg( size() + } + break; + + case QEvent::Leave: + m_timer.stop(); + break; + + // Xlib.h sucks fucking balls!!!!11!!1! + #undef FocusOut + case QEvent::FocusOut: + // if the user summons some dialog via a shortcut or whatever we need to ensure + // the mouse gets shown, because if it is modal, we won't get mouse events after + // it is shown! This works because we are always the focus widget. + // @see MainWindow::MainWindow where we setFocusProxy() + case QEvent::Enter: + case QEvent::MouseMove: + case QEvent::MouseButtonPress: + unsetCursor(); + if( hasFocus() ) + // see above comment + m_timer.start( CURSOR_HIDE_TIMEOUT, true ); + break; + + case QEvent::MouseButtonDblClick: + Codeine::action( "fullscreen" )->activate(); + break; + + default: ; + } + + if( !m_xine ) + return QWidget::event( e ); + + switch( e->type() ) + { + case QEvent::Close: + stop(); + return false; + + case VideoWindow::ExposeEvent: + //see VideoWindow::x11Event() + + return true; + + // Xlib.h sucks fucking balls!!!!11!!1! + #undef KeyPress + case QEvent::KeyPress: { + if( m_url.protocol() != "dvd" ) + // let MainWindow handle this + return QWidget::event( e ); + + //FIXME left and right keys don't work during DVDs + + int keyCode = XINE_EVENT_INPUT_UP; + + //#define XINE_EVENT_INPUT_UP 110 + //#define XINE_EVENT_INPUT_DOWN 111 + //#define XINE_EVENT_INPUT_LEFT 112 + //#define XINE_EVENT_INPUT_RIGHT 113 + //#define XINE_EVENT_INPUT_SELECT 114 + + switch( static_cast(e)->key() ) { + case Key_Return: + case Key_Enter: keyCode++; + case Key_Right: keyCode++; + case Key_Left: keyCode++; + case Key_Down: keyCode++; + case Key_Up: + { + //this whole shebang is cheeky as xine doesn't + //guarentee the codes will stay the same + + xine_event_t xineEvent; + + xineEvent.type = keyCode; + xineEvent.data = NULL; + xineEvent.data_length = 0; + + xine_event_send( m_stream, &xineEvent ); + + return true; + } + default: + return false; + } + } + + case QEvent::MouseButtonPress: + + #define mouseEvent static_cast(e) + + if( mouseEvent->button() != Qt::LeftButton ) + return false; + + mouseEvent->accept(); + + //FALL THROUGH + + case QEvent::MouseMove: + { + x11_rectangle_t x11Rect; + xine_event_t xineEvent; + xine_input_data_t xineInput; + + x11Rect.x = mouseEvent->x(); + x11Rect.y = mouseEvent->y(); + x11Rect.w = 0; + x11Rect.h = 0; + + xine_gui_send_vo_data( m_stream, XINE_GUI_SEND_TRANSLATE_GUI_TO_VIDEO, (void*)&x11Rect ); + + xineEvent.type = e->type() == QEvent::MouseMove ? XINE_EVENT_INPUT_MOUSE_MOVE : XINE_EVENT_INPUT_MOUSE_BUTTON; + xineEvent.data = &xineInput; + xineEvent.data_length = sizeof( xine_input_data_t ); + xineInput.button = 1; //HACK e->type() == QEvent::MouseMove ? 0 : 1; + xineInput.x = x11Rect.x; + xineInput.y = x11Rect.y; + xine_event_send( m_stream, &xineEvent ); + + return e->type() == QEvent::MouseMove ? false : true; + + #undef mouseEvent + } + + case QEvent::Wheel: + { + //TODO seek amount should depend on the length, basically seek at most say 30s, and at least 0.5s + //TODO this is replicated (somewhat) in MainWindow::keyPressEvent + + int pos, time, length; + xine_get_pos_length( m_stream, &pos, &time, &length ); + pos += int(std::log10( (double)length ) * static_cast(e)->delta()); + + seek( pos > 0 ? (uint)pos : 0 ); + + return true; + } + + default: ; + } + + return QWidget::event( e ); +} + +bool +VideoWindow::x11Event( XEvent *e ) +{ + if( m_stream && e->type == Expose && e->xexpose.count == 0 ) { + xine_gui_send_vo_data( + m_stream, + XINE_GUI_SEND_EXPOSE_EVENT, + e ); + + return true; + } + + return false; +} + +void +VideoWindow::hideCursor() +{ + setCursor( Qt::BlankCursor ); +} + +QSize +VideoWindow::sizeHint() const //virtual +{ + QSize s = TheStream::profile()->readSizeEntry( "Preferred Size" ); + + if( !s.isValid() ) + s = TheStream::defaultVideoSize(); + + if( s.isValid() && !s.isNull() ) + return s; + + return minimumSizeHint(); +} + +QSize +VideoWindow::minimumSizeHint() const //virtual +{ + const int x = fontMetrics().width( "x" ) * 4; + + return QSize( x * 12, x * 4 ); //FIXME +} + +void +VideoWindow::resetZoom() +{ + TheStream::profile()->deleteEntry( "Preferred Size" ); + topLevelWidget()->adjustSize(); +} + +} //namespace Codeine diff --git a/src/app/volumeAction.cpp b/src/app/volumeAction.cpp new file mode 100644 index 0000000..4215640 --- /dev/null +++ b/src/app/volumeAction.cpp @@ -0,0 +1,114 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include +#include +#include +#include +#include +#include + +#include "debug.h" +#include "volumeAction.h" +#include "volumeAction.moc" +#include "xineEngine.h" + + +class VolumeSlider : public QFrame +{ +public: + VolumeSlider( QWidget *parent ) + : QFrame( parent ) + { + slider = new QSlider( Qt::Vertical, this, "volume" ); + label = new QLabel( this ); + + QBoxLayout *lay = new QVBoxLayout( this ); + lay->addWidget( slider, 0, Qt::AlignHCenter ); + lay->addWidget( label, 0, Qt::AlignHCenter ); + lay->setMargin( 4 ); + + slider->setRange( 0, 100 ); + + setFrameStyle( QFrame::Plain | QFrame::Box ); + setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed ); + + hide(); + } + + QLabel *label; + QSlider *slider; +}; + + +VolumeAction::VolumeAction( KToolBar *bar, KActionCollection *ac ) + : KToggleAction( i18n("Volume"), "volume", Qt::Key_1, 0, 0, ac, "volume" ) + , m_anchor( 0 ) +{ + m_widget = new VolumeSlider( bar->topLevelWidget() ); + + connect( this, SIGNAL(toggled( bool )), SLOT(toggled( bool )) ); + connect( m_widget->slider, SIGNAL(sliderMoved( int )), SLOT(sliderMoved( int )) ); + connect( m_widget->slider, SIGNAL(sliderMoved( int )), Codeine::engine(), SLOT(setStreamParameter( int )) ); + connect( m_widget->slider, SIGNAL(sliderReleased()), SLOT(sliderReleased()) ); +} + +int +VolumeAction::plug( QWidget *bar, int index ) +{ + DEBUG_BLOCK + + int const id = KAction::plug( bar, index ); + + m_anchor = (QWidget*)bar->child( "toolbutton_volume" ); //KAction creates it with this name + m_anchor->installEventFilter( this ); //so we can keep m_widget anchored + + return id; +} + +void +VolumeAction::toggled( bool const b ) +{ + DEBUG_BLOCK + + m_widget->raise(); + m_widget->setShown( b ); +} + +void +VolumeAction::sliderMoved( int v ) +{ + v = 100 - v; //Qt sliders are wrong way round when vertical + + QString const t = QString::number( v ) + '%'; + + setToolTip( i18n( "Volume: %1" ).arg( t ) ); + m_widget->label->setText( t ); +} + +bool +VolumeAction::eventFilter( QObject *o, QEvent *e ) +{ + switch (e->type()) { + case QEvent::Move: + case QEvent::Resize: { + QWidget const * const &a = m_anchor; + + m_widget->move( a->mapTo( m_widget->parentWidget(), QPoint( 0, a->height() ) ) ); + m_widget->resize( a->width(), m_widget->sizeHint().height() ); + return false; + } + + //TODO one click method, flawed currently in fullscreen mode by palette change in mainwindow.cpp +/* case QEvent::MouseButtonPress: + m_widget->show(); + break; + + case QEvent::MouseButtonRelease: + m_widget->hide(); + break;*/ + + default: + return false; + } +} diff --git a/src/app/volumeAction.h b/src/app/volumeAction.h new file mode 100644 index 0000000..6c0c376 --- /dev/null +++ b/src/app/volumeAction.h @@ -0,0 +1,29 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_VOLUME_ACTION_H +#define CODEINE_VOLUME_ACTION_H + +#include + +class VolumeAction : public KToggleAction +{ + Q_OBJECT + + QWidget *m_anchor; + class VolumeSlider *m_widget; + + virtual bool eventFilter( QObject *o, QEvent *e ); + + virtual int plug( QWidget*, int ); + +private slots: + void toggled( bool ); + void sliderMoved( int ); + void sliderReleased() { setChecked( false ); toggled( false ); } + +public: + VolumeAction( KToolBar *anchor, KActionCollection *ac ); +}; + +#endif diff --git a/src/app/xineConfig.cpp b/src/app/xineConfig.cpp new file mode 100644 index 0000000..70ca11a --- /dev/null +++ b/src/app/xineConfig.cpp @@ -0,0 +1,321 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "debug.h" +#include // XineConfigDialog::ctor -> to get the iconloader +#include +#include // XineConfigDialog::ctor +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xineConfig.h" + +QString i18n(const char *text); + + +KDialogBase *XineConfigDialog::s_instance = 0; + + +namespace Codeine +{ + void + showXineConfigurationDialog( QWidget *parent, xine_t *xine ) + { + XineConfigDialog d( xine, parent ); + if( d.exec() == QDialog::Accepted ) + d.saveSettings(); + } +} + + +class TabWidget : public QTabWidget +{ +public: + TabWidget( QWidget *parent ) : QTabWidget( parent ) {} + + virtual QSize sizeHint() const + { + // Qt gives a stupid default sizeHint for this widget + return QSize( + reinterpret_cast(tabBar())->sizeHint().width() + 5, + QTabWidget::sizeHint().height() ); + } +}; + + +///@class XineConfigDialog + +XineConfigDialog::XineConfigDialog( xine_t *xine, QWidget *parent ) + : KDialogBase( parent, "xine_config_dialog", + true, //modal + i18n("Configure xine"), User1 | Stretch | Ok | Cancel, + Ok, //default button + false, //draw separator + KStdGuiItem::reset() ) + , m_xine( xine ) +{ + DEBUG_BLOCK + + s_instance = this; + const int METRIC = fontMetrics().width( 'x' ); + const int METRIC_3B2 = (3*METRIC)/2; + + QVBox *box = new QVBox( this ); + box->setSpacing( METRIC ); + setMainWidget( box ); + + { + QHBox *hbox = new QHBox( box ); + hbox->setSpacing( METRIC_3B2 ); + hbox->setMargin( METRIC_3B2 ); + QPixmap info = kapp->iconLoader()->loadIcon( "messagebox_info", KIcon::NoGroup, KIcon::SizeMedium, KIcon::DefaultState, 0, true ); + QLabel *label = new QLabel( hbox ); + label->setPixmap( info ); + label->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ); + label = new QLabel( i18n( + "xine's defaults are usually sensible and should not require modification. " + "However, full configurability is provided for your pleasure ;-)." ), hbox ); + label->setAlignment( QLabel::WordBreak | QLabel::AlignVCenter ); + } + + //FIXME after many hours I have discovered that this + // widget somehow sets the minSize of this widget to 0,0 + // whenever you resize the widget. WTF? + TabWidget *tabs = new TabWidget( box ); + + + class XineConfigEntryIterator { + xine_t *m_xine; + xine_cfg_entry_t m_entry; + bool m_valid; + public: + XineConfigEntryIterator( xine_t *xine ) : m_xine( xine ) { m_valid = xine_config_get_first_entry( m_xine, &m_entry ); } + inline XineConfigEntryIterator &operator++() { m_valid = xine_config_get_next_entry( m_xine, &m_entry ); return *this; } + inline xine_cfg_entry_t *operator*() { return m_valid ? &m_entry : 0; } + }; + + + QGridLayout *grid = 0; + QString currentPage; + QScrollView *view = 0; + parent = 0; + + for( XineConfigEntryIterator it( m_xine ); *it; ++it ) + { + const QString pageName = QString::fromUtf8( (*it)->key ).section( '.', 0, 0 ); + + if( (QStringList() << "ui" << "effects" << "subtitles").contains( pageName ) ) + continue; + + if( pageName != currentPage ) { + if( view ) + //NOTE won't be executed for last tab + view->viewport()->setMinimumWidth( grid->sizeHint().width() ); // seems necessary + + QString pageTitle = pageName; + pageTitle[0] = pageTitle[0].upper(); + + tabs->addTab( view = new QScrollView, pageTitle ); + view->setResizePolicy( QScrollView::AutoOneFit ); + view->setHScrollBarMode( QScrollView::AlwaysOff ); + view->setFrameShape( QFrame::NoFrame ); + view->addChild( parent = new QWidget( view->viewport() ) ); + + QBoxLayout *layout = new QVBoxLayout( parent, /*margin*/METRIC_3B2, /*spacing*/0 ); + + parent = new QFrame( parent ); + static_cast(parent)->setFrameStyle( QFrame::Panel | QFrame::Raised ); + static_cast(parent)->setLineWidth( 2 ); + grid = new QGridLayout( parent, /*rows*/0, /*cols*/2, /*margin*/20, /*spacing*/int(METRIC*2.5) ); + grid->setColStretch( 0, 3 ); + grid->setColStretch( 1, 2 ); + + layout->addWidget( parent, 0 ); + layout->addStretch( 1 ); + + currentPage = pageName; + } + + m_entrys.append( new XineConfigEntry( parent, grid, *it ) ); + } + + //finishing touches + m_entrys.setAutoDelete( true ); + enableButton( Ok, false ); + enableButton( User1, false ); + + Q_ASSERT( !isUnsavedSettings() ); +} + +void +XineConfigDialog::slotHelp() +{ + /// HACK called when a widget's input value changes + + const bool b = isUnsavedSettings(); + enableButton( Ok, b ); + enableButton( User1, b ); +} + +void +XineConfigDialog::slotUser1() +{ + for( QPtrListIterator it( m_entrys ); *it != 0; ++it ) + (*it)->reset(); + + slotHelp(); +} + +bool +XineConfigDialog::isUnsavedSettings() const +{ + for( QPtrListIterator it( m_entrys ); *it != 0; ++it ) + if( (*it)->isChanged() ) + return true; + + return false; +} + +#include +void +XineConfigDialog::saveSettings() +{ + for( XineConfigEntry *entry = m_entrys.first(); entry; entry = m_entrys.next() ) + if( entry->isChanged() ) + entry->save( m_xine ); + + xine_config_save( m_xine, QFile::encodeName( QDir::homeDirPath() + "/.xine/config" ) ); +} + + +///@class XineConfigEntry + +XineConfigEntry::XineConfigEntry( QWidget *parent, QGridLayout *grid, xine_cfg_entry_t *entry ) + : m_widget( 0 ) + , m_key( entry->key ) + , m_string( entry->str_value ) + , m_number( entry->num_value ) +{ + QWidget *&w = m_widget; + const char *signal = 0; + const int row = grid->numRows(); + + QString description_text = QString::fromUtf8( entry->description ); + description_text[0] = description_text[0].upper(); + + switch( entry->type ) + { + case XINE_CONFIG_TYPE_STRING: { + w = new KLineEdit( m_string, parent ); + signal = SIGNAL(textChanged( const QString& )); + break; + } + case XINE_CONFIG_TYPE_ENUM: { + w = new KComboBox( parent ); + for( int i = 0; entry->enum_values[i]; ++i ) + ((KComboBox*)w)->insertItem( QString::fromUtf8( entry->enum_values[i] ) ); + ((KComboBox*)w)->setCurrentItem( m_number ); + signal = SIGNAL(activated( int )); + break; + } + case XINE_CONFIG_TYPE_RANGE: + case XINE_CONFIG_TYPE_NUM: { + w = new QSpinBox( + QMIN( m_number, entry->range_min ), // xine bug, sometimes the min and max ranges + QMAX( m_number, entry->range_max ), // are both 0 even though this is bullshit + 1, parent ); + ((QSpinBox*)w)->setValue( m_number ); + signal = SIGNAL(valueChanged( int )); + break; + } + case XINE_CONFIG_TYPE_BOOL: { + w = new QCheckBox( description_text, parent ); + ((QCheckBox*)w)->setChecked( m_number ); + + connect( w, SIGNAL(toggled( bool )), XineConfigDialog::instance(), SLOT(slotHelp()) ); + QToolTip::add( w, "" + QString::fromUtf8( entry->help ) ); + grid->addMultiCellWidget( w, row, row, 0, 1 ); + return; //no need for a description label + } + default: + ; + } + + connect( w, signal, XineConfigDialog::instance(), SLOT(slotHelp()) ); + + QLabel *description = new QLabel( description_text + ':', parent ); + description->setAlignment( QLabel::WordBreak | QLabel::AlignVCenter ); + + const QString tip = "" + QString::fromUtf8( entry->help ); + QToolTip::add( w, tip ); + QToolTip::add( description, tip ); + +// grid->addWidget( description, row, 0, Qt::AlignVCenter ); + grid->addWidget( w, row, 1, Qt::AlignTop ); +} + +bool +XineConfigEntry::isChanged() const +{ + #define _( x ) static_cast(m_widget) + + switch( classType( m_widget->className() ) ) { + case LineEdit: return _(KLineEdit)->text().utf8() != m_string; + case ComboBox: return _(KComboBox)->currentItem() != m_number; + case SpinBox: return _(QSpinBox)->value() != m_number; + case CheckBox: return _(QCheckBox)->isChecked() != m_number; + } + return false; +} + +void +XineConfigEntry::reset() +{ + // this is because we only get called by the XineConfigDialog reset button + // and we don't want to cause a check for Ok/Reset button enabled state for + // every XineConfigEntry + m_widget->blockSignals( true ); + + switch( classType( m_widget->className() ) ) { + case LineEdit: _(KLineEdit)->setText( m_string ); break; + case ComboBox: _(KComboBox)->setCurrentItem( m_number ); break; + case SpinBox: _(QSpinBox)->setValue( m_number ); break; + case CheckBox: _(QCheckBox)->setChecked( (bool)m_number ); break; + } + m_widget->blockSignals( false ); +} + +void +XineConfigEntry::save( xine_t *xine ) +{ + xine_cfg_entry_t ent; + + if( xine_config_lookup_entry( xine, key(), &ent ) ) + { + switch( classType( m_widget->className() ) ) { + case LineEdit: m_string = _(KLineEdit)->text().utf8(); break; + case ComboBox: m_number = _(KComboBox)->currentItem(); break; + case SpinBox: m_number = _(QSpinBox)->value(); break; + case CheckBox: m_number = _(QCheckBox)->isChecked(); break; + } + + ent.str_value = qstrdup( m_string ); + ent.num_value = m_number; + + debug() << "Saving setting: " << key() << endl; + xine_config_update_entry( xine, &ent ); + } + else + Debug::warning() << "Couldn't save: " << key() << endl; + + #undef _ +} diff --git a/src/app/xineConfig.h b/src/app/xineConfig.h new file mode 100644 index 0000000..d7999d5 --- /dev/null +++ b/src/app/xineConfig.h @@ -0,0 +1,69 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef XINECONFIG_H +#define XINECONFIG_H + +#include +#include + +class KComboBox; +class KLineEdit; +class QCheckBox; +class QGridLayout; +class QSpinBox; + +typedef struct xine_s xine_t; +typedef struct xine_cfg_entry_s xine_cfg_entry_t; + + +///stores a single config entry of the config file + +class XineConfigEntry : public QObject +{ + enum ClassType { LineEdit, ComboBox, SpinBox, CheckBox }; + + QWidget *m_widget; + QCString m_key; + QCString m_string; + int m_number; + + static inline ClassType classType( const QCString &name ) + { + return name == "KLineEdit" ? LineEdit + : name == "KComboBox" ? ComboBox + : name == "QSpinBox" ? SpinBox : CheckBox; + } + +public: + XineConfigEntry( QWidget *parent, QGridLayout*, xine_cfg_entry_t* ); + + bool isChanged() const; + void save( xine_t* ); + void reset(); + + inline const QCString &key() const { return m_key; } +}; + + +class XineConfigDialog : public KDialogBase +{ + static KDialogBase *s_instance; + + QPtrList m_entrys; + xine_t *m_xine; + +public: + XineConfigDialog( xine_t *xine, QWidget *parent ); + + bool isUnsavedSettings() const; + void saveSettings(); + + static KDialogBase *instance() { return s_instance; } + +protected: + virtual void slotUser1(); + virtual void slotHelp(); +}; + +#endif diff --git a/src/app/xineEngine.cpp b/src/app/xineEngine.cpp new file mode 100644 index 0000000..58069c5 --- /dev/null +++ b/src/app/xineEngine.cpp @@ -0,0 +1,876 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#define CODEINE_DEBUG_PREFIX "engine" + +#include "actions.h" //::seek() FIXME unfortunate +#include //the fade out +#include "config.h" +#include "debug.h" +#include +#include +#include "mxcl.library.h" +#include //::sendEvent() +#include //record() +#include //::exists() +#include "slider.h" +#include "theStream.h" +#include +#include "xineEngine.h" +#include "xineScope.h" + + +#define XINE_SAFE_MODE 1 + +extern "C" { void _debug( const char *string ) { debug() << string; } } //FIXME + + +namespace Codeine { + + +VideoWindow *VideoWindow::s_instance = 0; + + +VideoWindow::VideoWindow( QWidget *parent ) + : QWidget( parent, "VideoWindow" ) + , m_osd( 0 ) + , m_stream( 0 ) + , m_eventQueue( 0 ) + , m_videoPort( 0 ) + , m_audioPort( 0 ) + , m_scope( 0 ) + , m_xine( 0 ) + , m_current_vpts( 0 ) +{ + DEBUG_BLOCK + + s_instance = this; + + setWFlags( Qt::WNoAutoErase ); + setMouseTracking( true ); + setAcceptDrops( true ); + setUpdatesEnabled( false ); //to stop Qt drawing over us + setPaletteBackgroundColor( Qt::black ); + setFocusPolicy( ClickFocus ); + + //TODO sucks + //TODO namespace this? + myList->next = myList; //init the buffer list +} + +VideoWindow::~VideoWindow() +{ + DEBUG_BLOCK + + eject(); + + // fade out volume on exit + if( m_stream && xine_get_status( m_stream ) == XINE_STATUS_PLAY ) { + int cum = 0; + for( int v = 99; v >= 0; v-- ) { + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_LEVEL, v ); + int sleep = int(32000 * (-std::log10( double(v + 1) ) + 2)); + + ::usleep( sleep ); + + cum += sleep; + } + + debug() << "Total sleep: " << cum << "x10^-6 s\n"; + + xine_stop( m_stream ); + + ::sleep( 1 ); + } + + //xine_set_param( m_stream, XINE_PARAM_IGNORE_VIDEO, 1 ); + + if( m_osd ) xine_osd_free( m_osd ); + if( m_stream ) xine_close( m_stream ); + if( m_eventQueue ) xine_event_dispose_queue( m_eventQueue ); + if( m_stream ) xine_dispose( m_stream ); + if( m_audioPort ) xine_close_audio_driver( m_xine, m_audioPort ); + if( m_videoPort ) xine_close_video_driver( m_xine, m_videoPort ); + if( m_scope ) xine_post_dispose( m_xine, m_scope ); + if( m_xine ) xine_exit( m_xine ); + + cleanUpVideo(); +} + +bool +VideoWindow::init() +{ + DEBUG_BLOCK + + initVideo(); + + debug() << "xine_new()\n"; + m_xine = xine_new(); + if( !m_xine ) + return false; + + #ifdef XINE_SAFE_MODE + xine_engine_set_param( m_xine, XINE_ENGINE_PARAM_VERBOSITY, 99 ); + #endif + + debug() << "xine_config_load()\n"; + xine_config_load( m_xine, QFile::encodeName( QDir::homeDirPath() + "/.xine/config" ) ); + + debug() << "xine_init()\n"; + xine_init( m_xine ); + + debug() << "xine_open_video_driver()\n"; + m_videoPort = xine_open_video_driver( m_xine, "auto", XINE_VISUAL_TYPE_X11, videoWindow()->x11Visual() ); + + debug() << "xine_open_audio_driver()\n"; + m_audioPort = xine_open_audio_driver( m_xine, "auto", NULL ); + + debug() << "xine_stream_new()\n"; + m_stream = xine_stream_new( m_xine, m_audioPort, m_videoPort ); + if( !m_stream ) + return false; + + // we do these after creating the stream as they are non-fatal + // and the messagebox creates a modal event loop that allows + // events that require a stream to have been created.. + if( !m_videoPort ) + MessageBox::error( i18n("xine was unable to initialize any video-drivers.") ); + if( !m_audioPort ) + MessageBox::error( i18n("xine was unable to initialize any audio-drivers.") ); + + debug() << "xine_osd_new()\n"; + m_osd = xine_osd_new( m_stream, 10, 10, 1000, 18 * 6 + 10 ); + if( m_osd ) { + xine_osd_set_font( m_osd, "sans", 18 ); + xine_osd_set_text_palette( m_osd, XINE_TEXTPALETTE_WHITE_BLACK_TRANSPARENT, XINE_OSD_TEXT1 ); + } + + #ifndef XINE_SAFE_MODE + debug() << "scope_plugin_new()\n"; + m_scope = scope_plugin_new( m_xine, m_audioPort ); + + //FIXME this one seems to make seeking unstable for Codeine, perhaps + xine_set_param( m_stream, XINE_PARAM_METRONOM_PREBUFFER, 6000 ); //less buffering, faster seeking.. + + // causes an abort currently + //xine_trick_mode( m_stream, XINE_TRICK_MODE_SEEK_TO_TIME, 1 ); + #endif + + + { + typedef QValueList List; + List params( List() + << XINE_PARAM_VO_HUE << XINE_PARAM_VO_SATURATION << XINE_PARAM_VO_CONTRAST << XINE_PARAM_VO_BRIGHTNESS + << XINE_PARAM_SPU_CHANNEL << XINE_PARAM_AUDIO_CHANNEL_LOGICAL << XINE_PARAM_VO_ASPECT_RATIO ); + + for( List::ConstIterator it = params.constBegin(), end = params.constEnd(); it != end; ++it ) + debug1( xine_get_param( m_stream, *it ) ); + } + + + debug() << "xine_event_create_listener_thread()\n"; + xine_event_create_listener_thread( m_eventQueue = xine_event_new_queue( m_stream ), &VideoWindow::xineEventListener, (void*)this ); + + //set the UI up to a default state + announceStateChange(); + + startTimer( 200 ); //prunes the scope + + return true; +} + +void +VideoWindow::eject() +{ + //WARNING! don't xine_stop or that, buggers up dtor + + if( m_url.isEmpty() ) + return; + + KConfig *profile = TheStream::profile(); // the config profile for this video file + + #define writeParameter( param, default ) { \ + const int value = xine_get_param( m_stream, param ); \ + const QString key = QString::number( param ); \ + if( value != default ) \ + profile->writeEntry( key, value ); \ + else \ + profile->deleteEntry( key ); } + + writeParameter( XINE_PARAM_VO_HUE, 32768 ); + writeParameter( XINE_PARAM_VO_SATURATION, 32772 ); + writeParameter( XINE_PARAM_VO_CONTRAST, 32772 ); + writeParameter( XINE_PARAM_VO_BRIGHTNESS, 32800 ) + writeParameter( XINE_PARAM_SPU_CHANNEL, -1 ); + writeParameter( XINE_PARAM_AUDIO_CHANNEL_LOGICAL, -1 ); + writeParameter( XINE_PARAM_VO_ASPECT_RATIO, 0 ); + + #undef writeParameter + + + if( xine_get_status( m_stream ) == XINE_STATUS_PLAY && //XINE_STATUS_PLAY = playing OR paused + length() - time() > 5000 ) // if we are really close to the end, don't remember the position + profile->writeEntry( "Position", position() ); + else + profile->deleteEntry( "Position" ); + + const QSize s = videoWindow()->size(); + const QSize defaultSize = TheStream::defaultVideoSize(); + if( s.width() == defaultSize.width() || s.height() == defaultSize.height() ) + profile->deleteEntry( "Preferred Size" ); + else + profile->writeEntry( "Preferred Size", s ); + + profile->sync(); + + m_url = KURL(); +} + +bool +VideoWindow::load( const KURL &url ) +{ + mxcl::WaitCursor allocateOnStack; + + eject(); //save profile for this video + + m_url = url; + + // only gets shown if there is an error generally, as no event processing + // occurs, so no paint event. This is fine IMO, TODO although if xine_open hangs + // due to something, it would be good to show the message... + emit statusMessage( i18n("Loading media: %1" ).arg( url.fileName() ) ); + + debug() << "xine_open()\n"; + if( xine_open( m_stream, url.url().local8Bit() ) ) + { + KConfig *profile = TheStream::profile(); + #define setParameter( param, default ) xine_set_param( m_stream, param, profile->readNumEntry( QString::number( param ), default ) ); + setParameter( XINE_PARAM_VO_HUE, 32768 ); + setParameter( XINE_PARAM_VO_SATURATION, 32772 ); + setParameter( XINE_PARAM_VO_CONTRAST, 32772 ); + setParameter( XINE_PARAM_VO_BRIGHTNESS, 32800 ) + setParameter( XINE_PARAM_SPU_CHANNEL, -1 ); + setParameter( XINE_PARAM_AUDIO_CHANNEL_LOGICAL, -1 ); + setParameter( XINE_PARAM_VO_ASPECT_RATIO, 0 ); + setParameter( XINE_PARAM_AUDIO_AMP_LEVEL, 100 ); + #undef setParameter + + videoWindow()->setShown( xine_get_stream_info( m_stream, XINE_STREAM_INFO_HAS_VIDEO ) ); + + //TODO popup message for no audio + //TODO popup message for no video + no audio + + #ifndef XINE_SAFE_MODE + // ensure old buffers are deleted + // FIXME leaves one erroneous buffer + timerEvent( 0 ); + + if( m_scope ) { + xine_post_out_t *source = xine_get_audio_source( m_stream ); + xine_post_in_t *target = (xine_post_in_t*)xine_post_input( m_scope, const_cast("audio in") ); + xine_post_wire( source, target ); + } + #endif + + announceStateChange(); + + return true; + } + + showErrorMessage(); + announceStateChange(); + m_url = KURL(); + return false; +} + +bool +VideoWindow::play( uint offset ) +{ + mxcl::WaitCursor allocateOnStack; + + const bool resume = offset > 0 && /*FIXME*/ m_url.protocol() != "dvd"; + if( resume ) + //HACK because we have to do xine_play() the audio "stutters" + // so we mute it and then unmute it to make it sound better + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_MUTE, 1 ); + + debug() << "xine_play()\n"; + if( xine_play( m_stream, offset, 0 ) ) + { + if( resume ) { + //we have to set this or it stays at 0 + Slider::instance()->setValue( offset ); + + // we come up paused if we are resuming playback from a previous session + pause(); + + // see above from HACK + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_MUTE, 0 ); + } + else + announceStateChange(); + + return true; + } + + showErrorMessage(); + return false; +} + +void +VideoWindow::record() +{ + xine_cfg_entry_t config; + + if( xine_config_lookup_entry( m_xine, "misc.save_dir", &config ) ) + { + //TODO which fricking KDE function tells me this? Who can tell, stupid KDE API + QDir d( QDir::home().filePath( "Desktop" ) ); + config.str_value = qstrdup( d.exists() //FIXME tiny-mem-leak, *shrug* + ? d.path().utf8() + : QDir::homeDirPath().utf8() ); + xine_config_update_entry( m_xine, &config ); + + const QString fileName = m_url.filename(); + + QString + url = m_url.url(); + url += "#save:"; + url += m_url.host(); + url += " ["; + url += QDate::currentDate().toString(); + url += ']'; + url += fileName.mid( fileName.findRev( '.' ) + 1 ).lower(); + + xine_open( m_stream, url.local8Bit() ); + xine_play( m_stream, 0, 0 ); + + emit statusMessage( i18n( "Recording to: %1" ).arg( url ) ); + + debug() << url << endl; + } + else + debug() << "unable to set misc.save_dir\n"; +} + +void +VideoWindow::stop() +{ + xine_stop( m_stream ); + + announceStateChange(); +} + +void +VideoWindow::pause() +{ + if( xine_get_status( m_stream ) == XINE_STATUS_STOP ) + play(); + + else if( m_url.protocol() == "http" ) + // we are playing and it's an HTTP stream + stop(); + + else if( xine_get_param( m_stream, XINE_PARAM_SPEED ) ) { + // do first because xine is slow to pause and is bad feedback otherwise + emit stateChanged( Engine::Paused ); + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_PAUSE ); + xine_set_param( m_stream, XINE_PARAM_AUDIO_CLOSE_DEVICE, 1); + showOSD( i18n( "Playback paused" ) ); + } + else { + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_NORMAL ); + announceStateChange(); + showOSD( i18n( "Playback resumed" ) ); + } +} + +void +VideoWindow::showErrorMessage() +{ + const QString name = m_url.fileName(); + + debug() << "xine_get_error()\n"; + switch( xine_get_error( m_stream ) ) + { + case XINE_ERROR_NO_INPUT_PLUGIN: + MessageBox::sorry( i18n("There is no input plugin that can read: %1.").arg( name ) ); + break; + case XINE_ERROR_NO_DEMUX_PLUGIN: + MessageBox::sorry( i18n("There is no demux plugin available for %1.").arg( name ) ); + break; + case XINE_ERROR_DEMUX_FAILED: + MessageBox::sorry( i18n("Demuxing failed for %1.").arg( name ) ); + break; + case XINE_ERROR_INPUT_FAILED: + case XINE_ERROR_MALFORMED_MRL: + case XINE_ERROR_NONE: + MessageBox::sorry( i18n("Internal error while attempting to play %1.").arg( name ) ); + break; + } +} + +Engine::State +VideoWindow::state() const +{ + //FIXME this is for the analyzer, but I don't like the analyzer being dodgy like this + if( !m_xine || !m_stream ) + return Engine::Uninitialised; + + switch( xine_get_status( m_stream ) ) + { + case XINE_STATUS_PLAY: return xine_get_param( m_stream, XINE_PARAM_SPEED ) ? Engine::Playing : Engine::Paused; + case XINE_STATUS_IDLE: return Engine::Empty; //FIXME this route never used! + case XINE_STATUS_STOP: + default: return m_url.isEmpty() ? Engine::Empty : Engine::Loaded; + } +} + +uint +VideoWindow::posTimeLength( PosTimeLength type ) const +{ + int pos = 0, time = 0, length = 0; + xine_get_pos_length( m_stream, &pos, &time, &length ); + + switch( type ) { + case Pos: return pos; + case Time: return time; + case Length: return length; + } + + return 0; //--warning +} + +uint +VideoWindow::volume() const +{ + //TODO I don't like the design + return xine_get_param( m_stream, XINE_PARAM_AUDIO_AMP_LEVEL ); +} + +void +VideoWindow::seek( uint pos ) +{ + bool wasPaused = false; + + // If we seek to the end the track ended event is sent, but it is + // delayed as it happens in xine-event loop and before that we are + // already processing the next seek event (if user uses mouse wheel + // or keyboard to seek) and this causes the ui to think video is + // stopped but xine is actually playing the track. Tada! + // TODO set state based on events from xine only + if( pos > 65534 ) + pos = 65534; + + switch( state() ) { + case Engine::Uninitialised: + //NOTE should never happen + Debug::warning() << "Seek attempt thwarted! xine not initialised!\n"; + return; + case Engine::Empty: + Debug::warning() << "Seek attempt thwarted! No media loaded!\n"; + return; + case Engine::Loaded: + // then the state is changing and we should announce it + play( pos ); + return; + case Engine::Paused: + // xine_play unpauses stream if stream was paused + // was broken at 1.0.1 still + wasPaused = true; + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_MUTE, 1 ); + break; + default: + ; + } + + if( !TheStream::canSeek() ) { + // for http streaming it is not a good idea to seek as xine freezes + // and/or just breaks, this is xine 1.0.1 + Debug::warning() << "We won't try to seek as the media is not seekable!\n"; + return; + } + + //TODO depend on a version that CAN seek in flacs! + if( m_url.path().endsWith( ".flac", false ) ) { + emit statusMessage( i18n("xine cannot currently seek in flac media") ); + return; + } + + //better feedback + //NOTE doesn't work! I can't tell why.. + Slider::instance()->QSlider::setValue( pos ); + Slider::instance()->repaint( false ); + + const bool fullscreen = toggleAction("fullscreen")->isChecked(); + if( fullscreen ) { + //TODO don't use OSD (sucks) show slider widget instead + QString osd = "["; + QChar separator = '|'; + + for( uint x = 0, y = int(pos / (65535.0/20.0)); x < 20; x++ ) { + if( x > y ) + separator = '.'; + osd += separator; + } + osd += ']'; + + xine_osd_clear( m_osd ); + xine_osd_draw_text( m_osd, 0, 0, osd.utf8(), XINE_OSD_TEXT1 ); + xine_osd_show( m_osd, 0 ); + } + + xine_play( m_stream, (int)pos, 0 ); + + if( fullscreen ) + //after xine_play because the hide command uses stream position + xine_osd_hide( m_osd, xine_get_current_vpts( m_stream ) + 180000 ); //2 seconds + + if( wasPaused ) + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_PAUSE ), + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_MUTE, 0 ); +} + +void +VideoWindow::setStreamParameter( int value ) +{ + QCString sender = this->sender()->name(); + int parameter; + + if( sender == "hue" ) + parameter = XINE_PARAM_VO_HUE; + else if( sender == "saturation" ) + parameter = XINE_PARAM_VO_SATURATION; + else if( sender == "contrast" ) + parameter = XINE_PARAM_VO_CONTRAST; + else if( sender == "brightness" ) + parameter = XINE_PARAM_VO_BRIGHTNESS; + else if( sender == "subtitle_channels_menu" ) + parameter = XINE_PARAM_SPU_CHANNEL, + value -= 2; + else if( sender == "audio_channels_menu" ) + parameter = XINE_PARAM_AUDIO_CHANNEL_LOGICAL, + value -= 2; + else if( sender == "aspect_ratio_menu" ) + parameter = XINE_PARAM_VO_ASPECT_RATIO; + else if( sender == "volume" ) + parameter = XINE_PARAM_AUDIO_AMP_LEVEL; + else + return; + + xine_set_param( m_stream, parameter, value ); +} + +const Engine::Scope& +VideoWindow::scope() +{ + using Analyzer::SCOPE_SIZE; + + static Engine::Scope scope( SCOPE_SIZE ); + + if( xine_get_status( m_stream ) != XINE_STATUS_PLAY ) + return scope; + + //prune the buffer list and update the m_current_vpts timestamp + timerEvent( 0 ); + + for( int channels = xine_get_stream_info( m_stream, XINE_STREAM_INFO_AUDIO_CHANNELS ), frame = 0; frame < SCOPE_SIZE; ) + { + MyNode *best_node = 0; + + for( MyNode *node = myList->next; node != myList; node = node->next ) + if( node->vpts <= m_current_vpts && (!best_node || node->vpts > best_node->vpts) ) + best_node = node; + + if( !best_node || best_node->vpts_end < m_current_vpts ) + break; + + int64_t + diff = m_current_vpts; + diff -= best_node->vpts; + diff *= 1<<16; + diff /= myMetronom->pts_per_smpls; + + const int16_t* + data16 = best_node->mem; + data16 += diff; + + diff += diff % channels; //important correction to ensure we don't overflow the buffer + diff /= channels; + + int + n = best_node->num_frames; + n -= diff; + n += frame; //clipping for # of frames we need + + if( n > SCOPE_SIZE ) + n = SCOPE_SIZE; //bounds limiting + + for( int a, c; frame < n; ++frame, data16 += channels ) { + for( a = c = 0; c < channels; ++c ) + a += data16[c]; + + a /= channels; + scope[frame] = a; + } + + m_current_vpts = best_node->vpts_end; + m_current_vpts++; //FIXME needs to be done for some reason, or you get situations where it uses same buffer again and again + } + + return scope; +} + +void +VideoWindow::timerEvent( QTimerEvent* ) +{ + /// here we prune the buffer list regularly + #ifndef XINE_SAFE_MODE + MyNode * const first_node = myList->next; + MyNode const * const list_end = myList; + + m_current_vpts = (xine_get_status( m_stream ) == XINE_STATUS_PLAY) + ? xine_get_current_vpts( m_stream ) + : std::numeric_limits::max(); + + for( MyNode *prev = first_node, *node = first_node->next; node != list_end; node = node->next ) + { + // we never delete first_node + // this maintains thread-safety + if( node->vpts_end < m_current_vpts ) { + prev->next = node->next; + + free( node->mem ); + free( node ); + + node = prev; + } + + prev = node; + } + #endif +} + +void +VideoWindow::customEvent( QCustomEvent *e ) +{ + switch( e->type() - 2000 ) { + case XINE_EVENT_UI_PLAYBACK_FINISHED: + emit stateChanged( Engine::TrackEnded ); + break; + + case XINE_EVENT_FRAME_FORMAT_CHANGE: + //TODO not ideal really + debug() << "XINE_EVENT_FRAME_FORMAT_CHANGE\n"; + break; + + case XINE_EVENT_UI_CHANNELS_CHANGED: + { + char s[128]; //apparently sufficient + + { + QStringList languages( "subtitle_channels_menu" ); + int channels = xine_get_stream_info( m_stream, XINE_STREAM_INFO_MAX_SPU_CHANNEL ); + for( int j = 0; j < channels; j++ ) + languages += xine_get_spu_lang( m_stream, j, s ) ? s : i18n("Channel %1").arg( j+1 ); + emit channelsChanged( languages ); + } + + { + QStringList languages( "audio_channels_menu" ); + int channels = xine_get_stream_info( m_stream, XINE_STREAM_INFO_MAX_AUDIO_CHANNEL ); + for( int j = 0; j < channels; j++ ) + languages += xine_get_audio_lang( m_stream, j, s ) ? s : i18n("Channel %1").arg( j+1 ); + emit channelsChanged( languages ); + } + break; + } + + case 1000: + #define message static_cast(e->data()) + emit statusMessage( *message ); + delete message; + break; + + case 1001: + MessageBox::sorry( (*message).arg( m_url.prettyURL() ) ); + delete message; + break; + + case 1002: + emit titleChanged( *message ); + delete message; + break; + #undef message + + default: + ; + } +} + +void +VideoWindow::xineEventListener( void *p, const xine_event_t* xineEvent ) +{ + if( !p ) + return; + + #define engine static_cast(p) + + switch( xineEvent->type ) { + case XINE_EVENT_UI_NUM_BUTTONS: debug() << "XINE_EVENT_UI_NUM_BUTTONS\n"; break; + case XINE_EVENT_MRL_REFERENCE: { + //FIXME this is not the right way, it will have bugs + debug() << "XINE_EVENT_MRL_REFERENCE\n"; + engine->m_url = QString::fromUtf8( ((xine_mrl_reference_data_t*)xineEvent->data)->mrl ); + QTimer::singleShot( 0, engine, SLOT(play()) ); + break; + } + case XINE_EVENT_DROPPED_FRAMES: debug() << "XINE_EVENT_DROPPED_FRAMES\n"; break; + + case XINE_EVENT_UI_PLAYBACK_FINISHED: + case XINE_EVENT_FRAME_FORMAT_CHANGE: + case XINE_EVENT_UI_CHANNELS_CHANGED: + { + QCustomEvent *ce; + ce = new QCustomEvent( 2000 + xineEvent->type ); + ce->setData( const_cast(xineEvent) ); + QApplication::postEvent( engine, ce ); + break; + } + + case XINE_EVENT_UI_SET_TITLE: + QApplication::postEvent( engine, new QCustomEvent( + QEvent::Type(3002), + new QString( QString::fromUtf8( static_cast(xineEvent->data)->str ) ) ) ); + break; + + case XINE_EVENT_PROGRESS: + { + xine_progress_data_t* pd = (xine_progress_data_t*)xineEvent->data; + + QString + msg = "%1 %2%"; + msg = msg.arg( QString::fromUtf8( pd->description ) ) + .arg( KGlobal::locale()->formatNumber( pd->percent, 0 ) ); + + QApplication::postEvent( engine, new QCustomEvent( QEvent::Type(3000), new QString( msg ) ) ); + break; + } + case XINE_EVENT_UI_MESSAGE: + { + debug() << "message received from xine\n"; + + xine_ui_message_data_t *data = (xine_ui_message_data_t *)xineEvent->data; + QString message; + + switch( data->type ) { + case XINE_MSG_NO_ERROR: + { + //series of \0 separated strings, terminated with a \0\0 + char str[2000]; + char *p = str; + for( char *msg = data->messages; !(*msg == '\0' && *(msg+1) == '\0'); ++msg, ++p ) + *p = *msg == '\0' ? '\n' : *msg; + *p = '\0'; + + debug() << str << endl; + + break; + } + + case XINE_MSG_ENCRYPTED_SOURCE: + message = i18n("The source is encrypted and can not be decrypted."); goto param; + case XINE_MSG_UNKNOWN_HOST: + message = i18n("The host is unknown for the URL: %1"); goto param; + case XINE_MSG_UNKNOWN_DEVICE: + message = i18n("The device name you specified seems invalid."); goto param; + case XINE_MSG_NETWORK_UNREACHABLE: + message = i18n("The network appears unreachable."); goto param; + case XINE_MSG_AUDIO_OUT_UNAVAILABLE: + message = i18n("Audio output unavailable; the device is busy."); goto param; + case XINE_MSG_CONNECTION_REFUSED: + message = i18n("The connection was refused for the URL: %1"); goto param; + case XINE_MSG_FILE_NOT_FOUND: + message = i18n("xine could not find the URL: %1"); goto param; + case XINE_MSG_PERMISSION_ERROR: + message = i18n("Access was denied for the URL: %1"); goto param; + case XINE_MSG_READ_ERROR: + message = i18n("The source cannot be read for the URL: %1"); goto param; + case XINE_MSG_LIBRARY_LOAD_ERROR: + message = i18n("A problem occurred while loading a library or decoder."); goto param; + + case XINE_MSG_GENERAL_WARNING: + case XINE_MSG_SECURITY: + default: + + if(data->explanation) + { + message += ""; + message += QString::fromUtf8( (char*) data + data->explanation ); + message += ""; + } + else break; //if no explanation then why bother! + + //FALL THROUGH + + param: + + message.prepend( "

" ); + message += "

"; + + if(data->parameters) + { + message += "xine says: "; + message += QString::fromUtf8( (char*) data + data->parameters); + message += ""; + } + else message += i18n("Sorry, no additional information is available."); + + QApplication::postEvent( engine, new QCustomEvent(QEvent::Type(3001), new QString(message)) ); + } + + } //case + } //switch + + #undef engine +} + +void +VideoWindow::toggleDVDMenu() +{ + xine_event_t e; + e.type = XINE_EVENT_INPUT_MENU1; + e.data = NULL; + e.data_length = 0; + + xine_event_send( m_stream, &e ); +} + +void +VideoWindow::showOSD( const QString &message ) +{ + if( m_osd ) { + xine_osd_clear( m_osd ); + xine_osd_draw_text( m_osd, 0, 0, message.utf8(), XINE_OSD_TEXT1 ); + xine_osd_show( m_osd, 0 ); + xine_osd_hide( m_osd, xine_get_current_vpts( m_stream ) + 180000 ); //2 seconds + } +} + +QString +VideoWindow::fileFilter() const +{ + char *supportedExtensions = xine_get_file_extensions( m_xine ); + + QString filter( "*." ); + filter.append( supportedExtensions ); + filter.remove( "txt" ); + filter.remove( "png" ); + filter.replace( ' ', " *." ); + + std::free( supportedExtensions ); + + return filter; +} + +} //namespace Codeine diff --git a/src/app/xineEngine.h b/src/app/xineEngine.h new file mode 100644 index 0000000..781bd72 --- /dev/null +++ b/src/app/xineEngine.h @@ -0,0 +1,159 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_VIDEOWINDOW_H +#define CODEINE_VIDEOWINDOW_H + +#include "codeine.h" +#include +#include +#include +#include + +typedef struct xine_s xine_t; +typedef struct xine_stream_s xine_stream_t; +typedef struct xine_video_port_s xine_video_port_t; +typedef struct xine_audio_port_s xine_audio_port_t; +typedef struct xine_event_queue_s xine_event_queue_t; +typedef struct xine_post_s xine_post_t; +typedef struct xine_osd_s xine_osd_t; + +namespace Engine { + typedef std::vector Scope; +} + + +namespace Codeine +{ + /** Functions declared here are defined in: + * xineEngine.cpp + * videoWindow.cpp + */ + class VideoWindow : public QWidget + { + Q_OBJECT + + enum PosTimeLength { Pos, Time, Length }; + + static VideoWindow *s_instance; + + VideoWindow( const VideoWindow& ); //disable + VideoWindow &operator=( const VideoWindow& ); //disable + + friend class TheStream; + friend VideoWindow* const engine(); + friend VideoWindow* const videoWindow(); + + public: + VideoWindow( QWidget *parent ); + ~VideoWindow(); + + bool init(); + void exit(); + + bool load( const KURL &url ); + bool play( uint = 0 ); + + uint position() const { return posTimeLength( Pos ); } + uint time() const { return posTimeLength( Time ); } + uint length() const { return posTimeLength( Length ); } + + uint volume() const; + + const Engine::Scope &scope(); + Engine::State state() const; + + operator xine_t*() const { return m_xine; } + operator xine_stream_t*() const { return m_stream; } + + public slots: + void pause(); + void record(); + void seek( uint ); + void stop(); + + ///special slot, see implementation to facilitate understanding + void setStreamParameter( int ); + + signals: + void stateChanged( Engine::State ); + void statusMessage( const QString& ); + void titleChanged( const QString& ); + void channelsChanged( const QStringList& ); + + private: + #ifdef HAVE_XINE_H + static void xineEventListener( void*, const xine_event_t* ); + #endif + + uint posTimeLength( PosTimeLength ) const; + void showErrorMessage(); + + virtual void customEvent( QCustomEvent* ); + virtual void timerEvent( QTimerEvent* ); + + void eject(); + + void announceStateChange() { emit stateChanged( state() ); } + + xine_osd_t *m_osd; + xine_stream_t *m_stream; + xine_event_queue_t *m_eventQueue; + xine_video_port_t *m_videoPort; + xine_audio_port_t *m_audioPort; + xine_post_t *m_scope; + xine_t *m_xine; + + int64_t m_current_vpts; + + KURL m_url; + + public: + QString fileFilter() const; + + public slots: + void toggleDVDMenu(); + void showOSD( const QString& ); + + /// Stuff to do with video and the video window/widget + private: + static void destSizeCallBack( void*, int, int, double, int*, int*, double* ); + static void frameOutputCallBack( void*, int, int, double, int*, int*, int*, int*, double*, int*, int* ); + + void initVideo(); + void cleanUpVideo(); + + public: + static const uint CURSOR_HIDE_TIMEOUT = 2000; + + virtual QSize sizeHint() const; + virtual QSize minimumSizeHint() const; + + void *x11Visual() const; + void becomePreferredSize(); + QImage captureFrame() const; + + enum { ExposeEvent = 3000 }; + + public slots: + void resetZoom(); + + private slots: + void hideCursor(); + + private: + virtual void contextMenuEvent( QContextMenuEvent* ); + virtual bool event( QEvent* ); + virtual bool x11Event( XEvent* ); + + double m_displayRatio; + QTimer m_timer; + }; + + //global function for general use by Codeine + //videoWindow() is const for Xlib-thread-safety reasons + inline VideoWindow* const videoWindow() { return VideoWindow::s_instance; } + inline VideoWindow* const engine() { return VideoWindow::s_instance; } +} + +#endif diff --git a/src/app/xineScope.c b/src/app/xineScope.c new file mode 100644 index 0000000..740d574 --- /dev/null +++ b/src/app/xineScope.c @@ -0,0 +1,148 @@ +/* Author: Max Howell , (C) 2004 + Copyright: See COPYING file that comes with this distribution */ + +/* gcc doesn't like inline for me */ +#define inline +/* need access to port_ticket */ +#define XINE_ENGINE_INTERNAL + +#include "xineScope.h" +#include +#include + + +static MyNode theList; +static metronom_t theMetronom; +static int myChannels = 0; + +MyNode* const myList = &theList; +metronom_t* const myMetronom = &theMetronom; + + +/* defined in xineEngine.cpp */ +extern void _debug( const char * ); + + +/************************* +* post plugin functions * +*************************/ + +static int +scope_port_open( xine_audio_port_t *port_gen, xine_stream_t *stream, uint32_t bits, uint32_t rate, int mode ) +{ + _debug( "scope_port_open()\n" ); + + #define port ((post_audio_port_t*)port_gen) + + _x_post_rewire( (post_plugin_t*)port->post ); + _x_post_inc_usage( port ); + + port->stream = stream; + port->bits = bits; + port->rate = rate; + port->mode = mode; + + myChannels = _x_ao_mode2channels( mode ); + + return port->original_port->open( port->original_port, stream, bits, rate, mode ); +} + +static void +scope_port_close( xine_audio_port_t *port_gen, xine_stream_t *stream ) +{ + _debug( "scope_port_close()\n" ); + + port->stream = NULL; + port->original_port->close( port->original_port, stream ); + + _x_post_dec_usage( port ); +} + +static void +scope_port_put_buffer( xine_audio_port_t *port_gen, audio_buffer_t *buf, xine_stream_t *stream ) +{ + MyNode *new_node; + const int num_samples = buf->num_frames * myChannels; + + /* we are too simple to handle 8bit */ + /* what does it mean when stream == NULL? */ + if( port->bits == 8 ) { + port->original_port->put_buffer( port->original_port, buf, stream ); return; } + + /* I keep my own metronom because xine wouldn't for some reason */ + memcpy( myMetronom, stream->metronom, sizeof(metronom_t) ); + + new_node = malloc( sizeof(MyNode) ); + new_node->vpts = myMetronom->got_audio_samples( myMetronom, buf->vpts, buf->num_frames ); + new_node->num_frames = buf->num_frames; + new_node->mem = malloc( num_samples * 2 ); + memcpy( new_node->mem, buf->mem, num_samples * 2 ); + + { + int64_t + K = myMetronom->pts_per_smpls; /*smpls = 1<<16 samples*/ + K *= num_samples; + K /= (1<<16); + K += new_node->vpts; + + new_node->vpts_end = K; + } + + /* pass data to original port */ + port->original_port->put_buffer( port->original_port, buf, stream ); + + /* finally we should append the current buffer to the list + * NOTE this is thread-safe due to the way we handle the list in the GUI thread */ + new_node->next = myList->next; + myList->next = new_node; + + #undef port +} + +static void +scope_dispose( post_plugin_t *this ) +{ + free( this ); +} + + +/************************ +* plugin init function * +************************/ + +xine_post_t* +scope_plugin_new( xine_t *xine, xine_audio_port_t *audio_target ) +{ + if( audio_target == NULL ) + return NULL; + + post_plugin_t *post_plugin = xine_xmalloc( sizeof(post_plugin_t) ); + + { + post_plugin_t *this = post_plugin; + post_in_t *input; + post_out_t *output; + post_audio_port_t *port; + + _x_post_init( this, 1, 0 ); + + port = _x_post_intercept_audio_port( this, audio_target, &input, &output ); + port->new_port.open = scope_port_open; + port->new_port.close = scope_port_close; + port->new_port.put_buffer = scope_port_put_buffer; + + this->xine_post.audio_input[0] = &port->new_port; + this->xine_post.type = PLUGIN_POST; + + this->dispose = scope_dispose; + } + + /* code is straight from xine_init_post() + can't use that function as it only dlopens the plugins + and our plugin is statically linked in */ + + post_plugin->running_ticket = xine->port_ticket; + post_plugin->xine = xine; + + return &post_plugin->xine_post; +} diff --git a/src/app/xineScope.h b/src/app/xineScope.h new file mode 100644 index 0000000..f2dae75 --- /dev/null +++ b/src/app/xineScope.h @@ -0,0 +1,38 @@ +/* Author: Max Howell , (C) 2004 + Copyright: See COPYING file that comes with this distribution + + This has to be a c file or for some reason it won't link! (GCC 3.4.1) +*/ + +#ifndef XINESCOPE_H +#define XINESCOPE_H + +/* need access to some stuff for scope time stamping */ +#define METRONOM_INTERNAL + +#include +#include + +typedef struct my_node_s MyNode; + +struct my_node_s +{ + MyNode *next; + int16_t *mem; + int num_frames; + int64_t vpts; + int64_t vpts_end; +}; + +extern metronom_t* const myMetronom; +extern MyNode* const myList; + +#ifdef __cplusplus +extern "C" +{ + xine_post_t* + scope_plugin_new( xine_t*, xine_audio_port_t* ); +} +#endif + +#endif diff --git a/src/codeine.h b/src/codeine.h new file mode 100644 index 0000000..24e0659 --- /dev/null +++ b/src/codeine.h @@ -0,0 +1,43 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_H +#define CODEINE_H + +// try to keep this file light. It gets included by +// practically every implementation and many headers + +namespace Engine +{ + enum State + { + Uninitialised = 0, + Empty = 1, + Loaded = 2, + Playing = 4, + Paused = 8, + TrackEnded = 16 + }; +} + +class QWidget; + +namespace Analyzer +{ + static const int SCOPE_SIZE_EXP = 9; + static const int SCOPE_SIZE = 1 << SCOPE_SIZE_EXP; +} + +namespace Codeine +{ + QWidget *mainWindow(); //defined in mainWindow.cpp +} + +/// used by mainWindow.h and xineEngine.h +int main( int, char** ); + +#define APP_VERSION "1.0.1" +#define APP_NAME "codeine" +#define PRETTY_NAME "Codeine" + +#endif diff --git a/src/debug.h b/src/debug.h new file mode 100644 index 0000000..a6b9baa --- /dev/null +++ b/src/debug.h @@ -0,0 +1,263 @@ +// Author: Max Howell , (C) 2003-5 +// Copyright: See COPYING file that comes with this distribution +// + +#ifndef CODEINE_DEBUG_H +#define CODEINE_DEBUG_H + +#include +#include +#include +#include + +class QApplication; ///@see Debug::Indent +extern QApplication *qApp; + + +/** + * @namespace Debug + * @short kdebug with indentation functionality and convenience macros + * @author Max Howell + * + * Usage: + * + * #define DEBUG_PREFIX "Blah" + * #include "debug.h" + * + * void function() + * { + * Debug::Block myBlock( __PRETTY_FUNCTION__ ); + * + * debug() << "output1" << endl; + * debug() << "output2" << endl; + * } + * + * Will output: + * + * app: BEGIN: void function() + * app: [Blah] output1 + * app: [Blah] output2 + * app: END: void function(): Took 0.1s + * + * @see Block + * @see CrashHelper + * @see ListStream + */ + + +namespace Debug +{ + inline QCString &indent() + { + static QCString indent; + return indent; + #if 0 + static timeval *stamp = 0; + + if( stamp == 0 ) { + stamp = new timeval; + return "[00:00] "; } + + timeval now; + gettimeofday( &now, 0 ); + now.tv_sec -= stamp->tv_sec; + + QString time( "[%1:%2]" ); + + return time.arg( now.tv_sec / 60, 2 ).arg( now.tv_sec % 60, 2 ).latin1() + indent; + #endif + } + + #ifdef NDEBUG + static inline kndbgstream debug() { return kndbgstream(); } + static inline kndbgstream warning() { return kndbgstream(); } + static inline kndbgstream error() { return kndbgstream(); } + static inline kndbgstream fatal() { return kndbgstream(); } + + static inline void debug1( QVariant v ) {} + + typedef kndbgstream Stream; + #else + #ifndef DEBUG_PREFIX + #define AMK_PREFIX "" + #else + #define AMK_PREFIX "[" DEBUG_PREFIX "] " + #endif + + //from kdebug.h + enum DebugLevels { + KDEBUG_INFO = 0, + KDEBUG_WARN = 1, + KDEBUG_ERROR = 2, + KDEBUG_FATAL = 3 + }; + + static inline kdbgstream debug() { return kdbgstream( indent(), 0, KDEBUG_INFO ) << AMK_PREFIX; } + static inline kdbgstream warning() { return kdbgstream( indent(), 0, KDEBUG_WARN ) << AMK_PREFIX << "[WARNING!] "; } + static inline kdbgstream error() { return kdbgstream( indent(), 0, KDEBUG_ERROR ) << AMK_PREFIX << "[ERROR!] "; } + static inline kdbgstream fatal() { return kdbgstream( indent(), 0, KDEBUG_FATAL ) << AMK_PREFIX; } + + /// convenience function + static inline void debug1( QVariant v ) { kdbgstream( indent(), 0, KDEBUG_INFO ) << v << endl; } + + typedef kdbgstream Stream; + + #undef AMK_PREFIX + #endif + + typedef kndbgstream DummyStream; +} + +using Debug::debug; +using Debug::debug1; + +/// Standard function announcer +#define DEBUG_FUNC_INFO kdDebug() << Debug::indent() << k_funcinfo << endl; + +/// Announce a line +#define DEBUG_LINE_INFO kdDebug() << Debug::indent() << k_funcinfo << "Line: " << __LINE__ << endl; + +/// Convenience macro for making a standard Debug::Block +#define DEBUG_BLOCK Debug::Block uniquelyNamedStackAllocatedStandardBlock( __PRETTY_FUNCTION__ ); + +#define DEBUG_INDENT Debug::indent() += " "; +#define DEBUG_UNINDENT { QCString &s = Debug::indent(); s.truncate( s.length() - 2 ); } + +/// Use this to remind yourself to finish the implementation of a function +#define DEBUG_NOTIMPLEMENTED warning() << "NOT-IMPLEMENTED: " << __PRETTY_FUNCTION__ << endl; + +/// Use this to alert other developers to stop using a function +#define DEBUG_DEPRECATED warning() << "DEPRECATED: " << __PRETTY_FUNCTION__ << endl; + + +namespace Debug +{ + /** + * @class Debug::Block + * @short Use this to label sections of your code + * + * Usage: + * + * void function() + * { + * Debug::Block myBlock( "section" ); + * + * debug() << "output1" << endl; + * debug() << "output2" << endl; + * } + * + * Will output: + * + * app: BEGIN: section + * app: [prefix] output1 + * app: [prefix] output2 + * app: END: section - Took 0.1s + * + */ + + class Block + { + timeval m_start; + const char *m_label; + + public: + Block( const char *label ) + : m_label( label ) + { + gettimeofday( &m_start, 0 ); + + kdDebug() << indent() << "BEGIN: " << label << "\n"; + DEBUG_INDENT + } + + ~Block() + { + timeval end; + gettimeofday( &end, 0 ); + + end.tv_sec -= m_start.tv_sec; + if( end.tv_usec < m_start.tv_usec) { + // Manually carry a one from the seconds field. + end.tv_usec += 1000000; + end.tv_sec--; + } + end.tv_usec -= m_start.tv_usec; + + double duration = double(end.tv_sec) + (double(end.tv_usec) / 1000000.0); + + DEBUG_UNINDENT + kdDebug() << indent() << "END__: " << m_label + << " - Took " << QString::number( duration, 'g', 3 ) << "s\n"; + } + }; + + + /** + * @name Debug::stamp() + * @short To facilitate crash/freeze bugs, by making it easy to mark code that has been processed + * + * Usage: + * + * { + * Debug::stamp(); + * function1(); + * Debug::stamp(); + * function2(); + * Debug::stamp(); + * } + * + * Will output (assuming the crash occurs in function2() + * + * app: Stamp: 1 + * app: Stamp: 2 + * + */ + + inline void stamp() + { + static int n = 0; + debug() << "| Stamp: " << ++n << endl; + } +} + + +namespace Debug +{ + /** + * @class Debug::List + * @short You can pass anything to this and it will output it as a list + * + * debug() << (Debug::List() << anInt << aString << aQStringList << aDouble) << endl; + */ + + typedef QValueList List; +} + + +#include + +namespace Codeine +{ + //FIXME this function is inlined, so this may cause linkage problems for some people.. + extern class VideoWindow* const videoWindow(); + + namespace MessageBox + { + static inline void error( const QString &message ) + { + KMessageBox::error( (QWidget*)videoWindow(), message ); + } + + static inline void sorry( const QString &message ) + { + KMessageBox::error( (QWidget*)videoWindow(), message ); + } + + static inline void information( const QString &message, const QString &title ) + { + KMessageBox::information( (QWidget*)videoWindow(), message, title ); + } + } +} + +#endif diff --git a/src/mxcl.library.cpp b/src/mxcl.library.cpp new file mode 100644 index 0000000..597a548 --- /dev/null +++ b/src/mxcl.library.cpp @@ -0,0 +1,19 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include "mxcl.library.h" +#include +#include + +namespace mxcl +{ + WaitCursor::WaitCursor() + { + QApplication::setOverrideCursor( KCursor::waitCursor() ); + } + + WaitCursor::~WaitCursor() + { + QApplication::restoreOverrideCursor(); + } +} diff --git a/src/mxcl.library.h b/src/mxcl.library.h new file mode 100644 index 0000000..feb71ef --- /dev/null +++ b/src/mxcl.library.h @@ -0,0 +1,28 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef MXCL_LIBRARY_H +#define MXCL_LIBRARY_H + + +namespace mxcl +{ + /// Allocate on stack, wait cursor will be shown during existance + struct WaitCursor + { + WaitCursor(); + ~WaitCursor(); + }; +} + + +/// almost always negates the need to #include in implementations +#include +QString i18n( const char *text ); + + +/// very useful for QStringLists +#define foreach( x ) \ + for( QStringList::ConstIterator it = x.constBegin(), end = x.constEnd(); it != end; ++it ) + +#endif diff --git a/src/part/SConscript b/src/part/SConscript new file mode 100644 index 0000000..c2bce67 --- /dev/null +++ b/src/part/SConscript @@ -0,0 +1,12 @@ +# Copyright 2005 Max Howell + +Import( "*" ) +myenv=env.Copy() + +## Additional paths for compiling the source files +## Always add '../' (top-level directory) because moc makes code that needs it +KDEaddpaths( ['./', '../', '../../'], myenv ) + +KDEaddlibs( ['qt-mt', 'kdecore', 'kdeui', 'kparts', 'xine'], myenv ) + +KDEshlib( "libcodeine", Split( "part.cpp xineEngine.cpp videoWindow.cpp toolbar.cpp ../mxcl.library.cpp" ), myenv ) diff --git a/src/part/part.cpp b/src/part/part.cpp new file mode 100644 index 0000000..20d1577 --- /dev/null +++ b/src/part/part.cpp @@ -0,0 +1,83 @@ +// Author: Max Howell , (C) 2005 +// Copyright: See COPYING file that comes with this distribution + +#include "codeine.h" +#include "debug.h" +#include +#include +#include "part.h" +#include +#include "toolbar.h" +#include "videoWindow.h" + +#include +#include + +namespace Codeine +{ + typedef KParts::GenericFactory Factory; +} + + +K_EXPORT_COMPONENT_FACTORY( libcodeine, Codeine::Factory ) + + +namespace Codeine +{ + Part::Part( QWidget *parentWidget, const char *widgetName, QObject *parent, const char *name, const QStringList& ) + : ReadOnlyPart( parent, name ) + , m_statusBarExtension( new KParts::StatusBarExtension( this ) ) + { + setInstance( Factory::instance() ); + setWidget( new VideoWindow( parentWidget, widgetName ) ); + + if( !videoWindow()->init() ) + //FIXME this will terminate the host, eg Konqueror + Debug::fatal() << "Couldn't init xine!\n"; + + KAction *play = new KToggleAction( i18n("Play"), "player_play", Qt::Key_Space, videoWindow(), SLOT(togglePlay()), actionCollection(), "play" ); + KAction *mute = new KToggleAction( i18n("Mute"), "player_mute", Qt::Key_M, videoWindow(), SLOT(toggleMute()), actionCollection(), "mute" ); + KToolBar *toolBar = new MouseOverToolBar( widget() ); + play->plug( toolBar ); + mute->plug( toolBar ); + m_slider = new QSlider( Qt::Horizontal, toolBar, "slider" ); + m_slider->setMaxValue( 65535 ); + toolBar->setStretchableWidget( m_slider ); + toolBar->addSeparator(); //FIXME ugly + + QObject *o = (QObject*)statusBar(); + connect( videoWindow(), SIGNAL(statusMessage( const QString& )), o, SLOT(message( const QString& )) ); + connect( videoWindow(), SIGNAL(titleChanged( const QString& )), o, SLOT(message( const QString& )) ); //FIXME + } + + bool + Part::openURL( const KURL &url ) + { + //FIXME nasty, we'd rather not do this way + killTimers(); + startTimer( 100 ); + + return videoWindow()->play( m_url = url ); + } + + bool + Part::closeURL() + { + m_url = KURL(); + videoWindow()->eject(); + return true; + } + + KAboutData* + Part::createAboutData() + { + // generic factory expects this on the heap + return new KAboutData( APP_NAME, PRETTY_NAME, APP_VERSION ); + } + + void + Part::timerEvent( QTimerEvent* ) + { + m_slider->setValue( videoWindow()->position() ); + } +} diff --git a/src/part/part.h b/src/part/part.h new file mode 100644 index 0000000..30467ac --- /dev/null +++ b/src/part/part.h @@ -0,0 +1,38 @@ +// Author: Max Howell , (C) 2005 +// Copyright: See COPYING file that comes with this distribution + +#ifndef CODEINE_PART_H +#define CODEINE_PART_H + +#include +#include +#include + +class KAboutData; +class QSlider; + + +namespace Codeine +{ + class Part : public KParts::ReadOnlyPart + { + public: + Part( QWidget*, const char*, QObject*, const char*, const QStringList& ); + + virtual bool openFile() { return false; } //pure virtual in base class + virtual bool openURL( const KURL& ); + virtual bool closeURL(); + + static KAboutData *createAboutData(); + + private: + KParts::StatusBarExtension *m_statusBarExtension; + QSlider *m_slider; + + KStatusBar *statusBar() { return m_statusBarExtension->statusBar(); } + + virtual void timerEvent( QTimerEvent* ); + }; +} + +#endif diff --git a/src/part/toolbar.cpp b/src/part/toolbar.cpp new file mode 100644 index 0000000..8939074 --- /dev/null +++ b/src/part/toolbar.cpp @@ -0,0 +1,44 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#include +#include +#include +#include "toolbar.h" + + +MouseOverToolBar::MouseOverToolBar( QWidget *parent ) + : KToolBar( parent ) +{ + parent->installEventFilter( this ); + move( 0, 0 ); //TODO necessary? + hide(); + + setPalette( QApplication::palette() ); //videoWindow palette has a black background +} + +bool +MouseOverToolBar::eventFilter( QObject *o, QEvent *e ) +{ + Q_ASSERT( o == parent() ); + + switch( e->type() ) + { + case QEvent::Resize: + resize( static_cast(e)->size().width(), sizeHint().height() ); + break; + + case QEvent::Enter: + show(); + break; + + case QEvent::Leave: + hide(); + break; + + default: + ; + } + + return false; +} \ No newline at end of file diff --git a/src/part/toolbar.h b/src/part/toolbar.h new file mode 100644 index 0000000..cd189d4 --- /dev/null +++ b/src/part/toolbar.h @@ -0,0 +1,18 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_TOOLBAR_H +#define CODEINE_TOOLBAR_H + +#include + + +class MouseOverToolBar : public KToolBar +{ + virtual bool eventFilter( QObject*, QEvent* ); + +public: + MouseOverToolBar( QWidget *parent ); +}; + +#endif diff --git a/src/part/videoWindow.cpp b/src/part/videoWindow.cpp new file mode 100644 index 0000000..d798b36 --- /dev/null +++ b/src/part/videoWindow.cpp @@ -0,0 +1,192 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#define CODEINE_DEBUG_PREFIX "videoWindow" + +#include +#include "debug.h" +#include //sendEvent() +#include +#include +#include "videoWindow.h" +#include //TODO this breaks compile for lots of people due to excessive macro content +#include //x11_visual_t + + +namespace Codeine { + + +VideoWindow *VideoWindow::s_instance = 0; + + +namespace X +{ + Display *d; + int s, w; +} + + +VideoWindow::VideoWindow( QWidget *parent, const char *name ) + : QWidget( parent, name ) + , m_osd( 0 ) + , m_stream( 0 ) + , m_eventQueue( 0 ) + , m_videoPort( 0 ) + , m_audioPort( 0 ) + , m_xine( 0 ) + , m_displayRatio( 1 ) +{ + s_instance = this; + + // with this Konqueror would crash on exit + // without this we may be unstable! + //XInitThreads(); + + show(); + + setWFlags( Qt::WNoAutoErase ); + setMouseTracking( true ); + setAcceptDrops( true ); + setUpdatesEnabled( false ); //to stop Qt drawing over us + setPaletteBackgroundColor( Qt::black ); + + X::d = XOpenDisplay( std::getenv("DISPLAY") ); + X::s = DefaultScreen( X::d ); + X::w = winId(); + + XLockDisplay( X::d ); + XSelectInput( X::d, X::w, ExposureMask ); + + { + using X::d; using X::s; + + //these are Xlib macros + double w = DisplayWidth( d, s ) * 1000 / DisplayWidthMM( d, s ); + double h = DisplayHeight( d, s ) * 1000 / DisplayHeightMM( d, s ); + + m_displayRatio = w / h; + } + + XUnlockDisplay( X::d ); + + connect( &m_timer, SIGNAL(timeout()), SLOT(hideCursor()) ); +} + +VideoWindow::~VideoWindow() +{ + DEBUG_BLOCK + + if( m_osd ) xine_osd_free( m_osd ); + if( m_stream ) xine_close( m_stream ); + if( m_eventQueue ) xine_event_dispose_queue( m_eventQueue ); + if( m_stream ) xine_dispose( m_stream ); + if( m_videoPort ) xine_close_video_driver( m_xine, m_videoPort ); + if( m_audioPort ) xine_close_audio_driver( m_xine, m_audioPort ); + if( m_xine ) xine_exit( m_xine ); + + XCloseDisplay( X::d ); +} + +void* +VideoWindow::x11Visual() const +{ + x11_visual_t* visual = new x11_visual_t; + + visual->display = X::d; + visual->screen = X::s; + visual->d = X::w; + visual->dest_size_cb = &VideoWindow::destSizeCallBack; + visual->frame_output_cb = &VideoWindow::frameOutputCallBack; + visual->user_data = (void*)this; + + return visual; +} + +void +VideoWindow::destSizeCallBack( + void* p, int /*video_width*/, int /*video_height*/, + double /*video_aspect*/, int* dest_width, + int* dest_height, double* dest_aspect ) +{ + if( !p ) + return; + + #define vw static_cast(p) + + *dest_width = vw->width(); + *dest_height = vw->height(); + *dest_aspect = vw->m_displayRatio; +} + +void +VideoWindow::frameOutputCallBack( + void* p, int video_width, int video_height, double video_aspect, + int* dest_x, int* dest_y, int* dest_width, int* dest_height, + double* dest_aspect, int* win_x, int* win_y ) +{ + if( !p ) + return; + + *dest_x = 0; + *dest_y = 0 ; + *dest_width = vw->width(); + *dest_height = vw->height(); + *win_x = vw->x(); + *win_y = vw->y(); + *dest_aspect = vw->m_displayRatio; + + // correct size with video aspect + // TODO what's this about? + if( video_aspect >= vw->m_displayRatio ) + video_width = int( double(video_width * video_aspect / vw->m_displayRatio + 0.5) ); + else + video_height = int( double(video_height * vw->m_displayRatio / video_aspect) + 0.5 ); + + #undef vw +} + +bool +VideoWindow::event( QEvent *e ) +{ + switch( e->type() ) + { + case QEvent::MouseMove: + case QEvent::MouseButtonPress: + unsetCursor(); + m_timer.start( CURSOR_HIDE_TIMEOUT, true ); + break; + + case QEvent::Close: + case QEvent::Hide: + xine_stop( m_stream ); + break; + + case QEvent::Leave: + m_timer.stop(); + break; + + default: + ; + } + + return QWidget::event( e ); +} + +bool +VideoWindow::x11Event( XEvent *e ) +{ + if( e->type == Expose && e->xexpose.count == 0 ) { + xine_gui_send_vo_data( m_stream, XINE_GUI_SEND_EXPOSE_EVENT, e ); + return true; + } + + return false; +} + +void +VideoWindow::hideCursor() +{ + setCursor( Qt::BlankCursor ); +} + +} //namespace Codeine diff --git a/src/part/videoWindow.h b/src/part/videoWindow.h new file mode 100644 index 0000000..7db72ab --- /dev/null +++ b/src/part/videoWindow.h @@ -0,0 +1,94 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#ifndef CODEINE_VIDEO_WINDOW_H +#define CODEINE_VIDEO_WINDOW_H + +#include "../codeine.h" +#include +#include +#include + +typedef struct xine_s xine_t; +typedef struct xine_stream_s xine_stream_t; +typedef struct xine_video_port_s xine_video_port_t; +typedef struct xine_audio_port_s xine_audio_port_t; +typedef struct xine_event_queue_s xine_event_queue_t; +typedef struct xine_post_s xine_post_t; +typedef struct xine_osd_s xine_osd_t; + + +namespace Codeine +{ + class VideoWindow : public QWidget + { + Q_OBJECT + + static VideoWindow *s_instance; + static const uint CURSOR_HIDE_TIMEOUT = 2000; + + friend VideoWindow* const videoWindow(); + + public: + VideoWindow( QWidget *parent, const char *name ); + ~VideoWindow(); + + bool init(); + + bool play( KURL ); + void eject(); + + int position(); + + signals: + void statusMessage( const QString& ); + void titleChanged( const QString& ); + + private: + /// @see xineEngine.cpp + #ifdef HAVE_XINE_H + static void xineEventListener( void*, const xine_event_t* ); + #endif + + void showErrorMessage(); //TODO don't use this, just show delayed message + + virtual void customEvent( QCustomEvent* ); + virtual bool x11Event( XEvent* ); + virtual bool event( QEvent* ); + + xine_osd_t *m_osd; + xine_stream_t *m_stream; + xine_event_queue_t *m_eventQueue; + xine_video_port_t *m_videoPort; + xine_audio_port_t *m_audioPort; + xine_t *m_xine; + + KURL m_url; + + private: + void *x11Visual() const; + + static void destSizeCallBack( void*, int, int, double, int*, int*, double* ); + static void frameOutputCallBack( void*, int, int, double, int*, int*, int*, int*, double*, int*, int* ); + + double m_displayRatio; + QTimer m_timer; + + public slots: + void togglePlay(); + void toggleMute(); + + private slots: + void hideCursor(); + + private: + /// prevent compiler generated functions + VideoWindow( const VideoWindow& ); + VideoWindow &operator=( const VideoWindow& ); + bool operator==( const VideoWindow& ); + }; + + inline VideoWindow* const videoWindow() { return VideoWindow::s_instance; } +} + +#endif diff --git a/src/part/xineEngine.cpp b/src/part/xineEngine.cpp new file mode 100644 index 0000000..2365f83 --- /dev/null +++ b/src/part/xineEngine.cpp @@ -0,0 +1,345 @@ +// (C) 2005 Max Howell (max.howell@methylblue.com) +// See COPYING file for licensing information + +#define CODEINE_DEBUG_PREFIX "engine" + +#include "debug.h" +#include +#include +#include "mxcl.library.h" +#include //::sendEvent() +#include //::play() +#include //QDir::homeDir() +#include +#include "videoWindow.h" + + +namespace Codeine { + + +bool +VideoWindow::init() +{ + mxcl::WaitCursor allocateOnStack; + + debug() << "xine_new()\n"; + m_xine = xine_new(); + if( !m_xine ) + return false; + + debug() << "xine_config_load()\n"; + xine_config_load( m_xine, QFile::encodeName( QDir::homeDirPath() + "/.xine/config" ) ); + + debug() << "xine_init()\n"; + xine_init( m_xine ); + + debug() << "xine_open_video_driver()\n"; + m_videoPort = xine_open_video_driver( m_xine, "auto", XINE_VISUAL_TYPE_X11, x11Visual() ); + + debug() << "xine_open_audio_driver()\n"; + m_audioPort = xine_open_audio_driver( m_xine, "auto", NULL ); + + debug() << "xine_stream_new()\n"; + m_stream = xine_stream_new( m_xine, m_audioPort, m_videoPort ); + if( !m_stream ) + return false; + + if( !m_audioPort ) + MessageBox::error( i18n("xine was unable to initialize any audio-drivers.") ); + if( !m_videoPort ) + MessageBox::error( i18n("xine was unable to initialize any video-drivers.") ); + + debug() << "xine_osd_new()\n"; + m_osd = xine_osd_new( m_stream, 10, 10, 1000, 18 * 6 + 10 ); + if( m_osd ) { + xine_osd_set_font( m_osd, "sans", 18 ); + xine_osd_set_text_palette( m_osd, XINE_TEXTPALETTE_WHITE_BLACK_TRANSPARENT, XINE_OSD_TEXT1 ); + } + + debug() << "xine_event_create_listener_thread()\n"; + xine_event_create_listener_thread( + m_eventQueue = xine_event_new_queue( m_stream ), + &VideoWindow::xineEventListener, (void*)this ); + + { /// set save directory + xine_cfg_entry_t config; + + if( xine_config_lookup_entry( m_xine, "misc.save_dir", &config ) ) { + const QCString dir = KGlobalSettings::desktopPath().local8Bit(); + config.str_value = qstrdup( dir ); + xine_config_update_entry( m_xine, &config ); + } + } + + return true; +} + +bool +VideoWindow::play( KURL url ) +{ + DEBUG_BLOCK + + m_url = url; + + mxcl::WaitCursor allocateOnStack; + + //TODO make sensible + if( url.protocol() == "http" ) { + /// automatically save http streams to Desktop folder + + const QString fileName = url.filename(); + + QString + u = url.url(); + u += "#save:"; + u += url.host(); + u += " ["; + u += QDate::currentDate().toString(); + u += ']'; + u += fileName.mid( fileName.findRev( '.' ) + 1 ).lower(); + + url = u; + } + + debug() << "About to load..\n"; + if( xine_open( m_stream, url.url().local8Bit() ) ) + { + debug() << "About to play..\n"; + if( xine_play( m_stream, 0, 0 ) ) + return true; + } + + showErrorMessage(); + return false; +} + +void +VideoWindow::togglePlay() +{ + if( xine_get_param( m_stream, XINE_PARAM_SPEED ) ) { + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_PAUSE ); + xine_set_param( m_stream, XINE_PARAM_AUDIO_CLOSE_DEVICE, 1); + } + else + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_NORMAL ); +} + +void +VideoWindow::toggleMute() +{ + bool const muted = xine_get_param( m_stream, XINE_PARAM_AUDIO_MUTE ); + xine_set_param( m_stream, XINE_PARAM_AUDIO_MUTE, !muted ); +} + +void +VideoWindow::eject() +{ + m_url = KURL(); + + xine_stop( m_stream ); +} + +int +VideoWindow::position() +{ + int pos = 0, time = 0, length = 0; + xine_get_pos_length( m_stream, &pos, &time, &length ); + + return pos; +} + +void +VideoWindow::showErrorMessage() +{ + const QString filename = m_url.fileName(); + + debug() << "xine_get_error()\n"; + + // NOTE these error messages are somewhat customised + // relative to the main application + // This is because when embedded in some other application + // the error messages have no context, so we must say that we are a video player! + + switch( xine_get_error( m_stream ) ) + { + case XINE_ERROR_NO_INPUT_PLUGIN: + MessageBox::sorry( i18n("The Codeine video player could not find an input plugin for '%1'.").arg( filename ) ); + break; + case XINE_ERROR_NO_DEMUX_PLUGIN: + MessageBox::sorry( i18n("The Codeine video player could not find a demux plugin for '%1'.").arg( filename ) ); + break; + case XINE_ERROR_DEMUX_FAILED: + MessageBox::sorry( i18n("The Codeine video player failed to demux '%1'; please check your xine installation.").arg( filename ) ); + break; + case XINE_ERROR_INPUT_FAILED: + case XINE_ERROR_MALFORMED_MRL: + case XINE_ERROR_NONE: + MessageBox::sorry( i18n("The Codeine video player reports an internal error; please check your xine installation.") ); + break; + } +} + +void +VideoWindow::customEvent( QCustomEvent *e ) +{ + switch( e->type() - 2000 ) { + case XINE_EVENT_UI_PLAYBACK_FINISHED: +//FIXME emit stateChanged( Engine::TrackEnded ); + break; + + case 1000: + #define message static_cast(e->data()) + emit statusMessage( *message ); + delete message; + break; + + case 1001: + MessageBox::sorry( (*message).arg( "FIXME" ) ); //FIXME + delete message; + break; + + case 1002: + emit titleChanged( *message ); + delete message; + break; + #undef message + + default: + ; + } +} + +void +VideoWindow::xineEventListener( void *p, const xine_event_t* xineEvent ) +{ + if( !p ) + return; + + #define engine static_cast(p) + + switch( xineEvent->type ) { + case XINE_EVENT_MRL_REFERENCE: + { + mxcl::WaitCursor allocateOnStack; + const char *mrl = ((xine_mrl_reference_data_t*)xineEvent->data)->mrl; + + debug() << "XINE_EVENT_MRL_REFERENCE: " << mrl << endl; + + if( xine_open( engine->m_stream, mrl ) ) + xine_play( engine->m_stream, 0, 0 ); + + break; + } + + case XINE_EVENT_UI_NUM_BUTTONS: debug() << "XINE_EVENT_UI_NUM_BUTTONS\n"; break; + case XINE_EVENT_DROPPED_FRAMES: debug() << "XINE_EVENT_DROPPED_FRAMES\n"; break; + + case XINE_EVENT_UI_PLAYBACK_FINISHED: + case XINE_EVENT_FRAME_FORMAT_CHANGE: + case XINE_EVENT_UI_CHANNELS_CHANGED: + { + QCustomEvent *ce; + ce = new QCustomEvent( 2000 + xineEvent->type ); + ce->setData( const_cast(xineEvent) ); + QApplication::postEvent( engine, ce ); + break; + } + + case XINE_EVENT_UI_SET_TITLE: + QApplication::postEvent( engine, new QCustomEvent( + QEvent::Type(3002), + new QString( QString::fromUtf8( static_cast(xineEvent->data)->str ) ) ) ); + break; + + case XINE_EVENT_PROGRESS: + { + xine_progress_data_t* pd = (xine_progress_data_t*)xineEvent->data; + + QString + msg = "%1 %2%"; + msg = msg.arg( QString::fromUtf8( pd->description ) ) + .arg( KGlobal::locale()->formatNumber( pd->percent, 0 ) ); + + QApplication::postEvent( engine, new QCustomEvent( QEvent::Type(3000), new QString( msg ) ) ); + break; + } + case XINE_EVENT_UI_MESSAGE: + { + debug() << "Message received from xine\n"; + + xine_ui_message_data_t *data = (xine_ui_message_data_t *)xineEvent->data; + QString message; + + switch( data->type ) { + case XINE_MSG_NO_ERROR: + { + //series of \0 separated strings, terminated with a \0\0 + char str[2000]; + char *p = str; + for( char *msg = data->messages; !(*msg == '\0' && *(msg+1) == '\0'); ++msg, ++p ) + *p = *msg == '\0' ? '\n' : *msg; + *p = '\0'; + + debug() << str << endl; + + break; + } + + case XINE_MSG_ENCRYPTED_SOURCE: + message = i18n("The source is encrypted and can not be decrypted."); goto param; + case XINE_MSG_UNKNOWN_HOST: + message = i18n("The host is unknown for the URL: %1"); goto param; + case XINE_MSG_UNKNOWN_DEVICE: + message = i18n("The device name you specified seems invalid."); goto param; + case XINE_MSG_NETWORK_UNREACHABLE: + message = i18n("The network appears unreachable."); goto param; + case XINE_MSG_AUDIO_OUT_UNAVAILABLE: + message = i18n("Audio output unavailable; the device is busy."); goto param; + case XINE_MSG_CONNECTION_REFUSED: + message = i18n("The connection was refused for the URL: %1"); goto param; + case XINE_MSG_FILE_NOT_FOUND: + message = i18n("xine could not find the URL: %1"); goto param; + case XINE_MSG_PERMISSION_ERROR: + message = i18n("Access was denied for the URL: %1"); goto param; + case XINE_MSG_READ_ERROR: + message = i18n("The source cannot be read for the URL: %1"); goto param; + case XINE_MSG_LIBRARY_LOAD_ERROR: + message = i18n("A problem occurred while loading a library or decoder."); goto param; + + case XINE_MSG_GENERAL_WARNING: + case XINE_MSG_SECURITY: + default: + + if(data->explanation) + { + message += ""; + message += QString::fromUtf8( (char*) data + data->explanation ); + message += ""; + } + else break; //if no explanation then why bother! + + //FALL THROUGH + + param: + + message.prepend( "

" ); + message += "

"; + + if(data->parameters) + { + message += "xine says: "; + message += QString::fromUtf8( (char*) data + data->parameters); + message += ""; + } + else message += i18n("Sorry, no additional information is available."); + + QApplication::postEvent( engine, new QCustomEvent(QEvent::Type(3001), new QString(message)) ); + } + + } //case + } //switch + + #undef engine +} + +} //namespace Codeine