commit 5f44f7b187093ef290315b7f8766b540a31de35f Author: Michele Calgaro Date: Sat Jun 13 22:45:28 2020 +0900 Initial code import from debian snapshot https://snapshot.debian.org/package/codeine/1.0.1-3.dfsg-3.1/ Signed-off-by: Michele Calgaro 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 0000000..f01fd7e Binary files /dev/null and b/misc/cr128-app-codeine.png differ diff --git a/misc/cr16-app-codeine.png b/misc/cr16-app-codeine.png new file mode 100644 index 0000000..dab56c9 Binary files /dev/null and b/misc/cr16-app-codeine.png differ diff --git a/misc/cr22-app-codeine.png b/misc/cr22-app-codeine.png new file mode 100644 index 0000000..c8ed199 Binary files /dev/null and b/misc/cr22-app-codeine.png differ diff --git a/misc/cr32-app-codeine.png b/misc/cr32-app-codeine.png new file mode 100644 index 0000000..6cbd81c Binary files /dev/null and b/misc/cr32-app-codeine.png differ diff --git a/misc/cr48-app-codeine.png b/misc/cr48-app-codeine.png new file mode 100644 index 0000000..f9f9180 Binary files /dev/null and b/misc/cr48-app-codeine.png differ diff --git a/misc/cr64-app-codeine.png b/misc/cr64-app-codeine.png new file mode 100644 index 0000000..da50713 Binary files /dev/null and b/misc/cr64-app-codeine.png differ 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 0000000..0c1ce52 Binary files /dev/null and b/scons/scons-mini.tar.bz2 differ 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