diff -Nru palapeli-4.12.3/debian/changelog palapeli-4.12.90/debian/changelog --- palapeli-4.12.3/debian/changelog 2014-03-04 20:18:33.000000000 +0000 +++ palapeli-4.12.90/debian/changelog 2014-03-19 12:06:57.000000000 +0000 @@ -1,3 +1,9 @@ +palapeli (4:4.12.90-0ubuntu1) trusty; urgency=medium + + * New upstream beta release + + -- Jonathan Riddell Wed, 19 Mar 2014 12:06:56 +0000 + palapeli (4:4.12.3-0ubuntu1) trusty; urgency=medium * New upstream bugfix release diff -Nru palapeli-4.12.3/debian/control palapeli-4.12.90/debian/control --- palapeli-4.12.3/debian/control 2014-03-04 20:18:33.000000000 +0000 +++ palapeli-4.12.90/debian/control 2014-03-19 12:06:57.000000000 +0000 @@ -13,8 +13,8 @@ Maximiliano Curia Build-Depends: kde-sc-dev-latest (>= 4:4.10.2), cmake, debhelper (>= 9), pkg-kde-tools (>= 0.14), - kdelibs5-dev (>= 4:4.12.3), - libkdegames-dev (>= 4:4.12.3), + kdelibs5-dev (>= 4:4.12.90), + libkdegames-dev (>= 4:4.12.90), shared-mime-info Standards-Version: 3.9.4 Homepage: http://games.kde.org/ diff -Nru palapeli-4.12.3/doc/index.docbook palapeli-4.12.90/doc/index.docbook --- palapeli-4.12.3/doc/index.docbook 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/doc/index.docbook 2014-03-10 18:48:34.000000000 +0000 @@ -1,7 +1,7 @@ Palapeli"> -Palapeli Slicer Collection"> +Palapeli Slicer Collection"> @@ -22,6 +22,11 @@ Majewsky
majewsky@gmx.net
+ + Ian + Wadham +
iandw.au@gmail.com
+
@@ -29,9 +34,13 @@ 2009, 2010 Johannes Löhnert, Stefan Majewsky + + 2014 + Ian Wadham + &FDLNotice; - 2010-11-29 - 1.2 + 2014-03-09 + 2.0 (&kde; 4.13) This is the manual for &palapeli;, a jigsaw puzzle game based on &kde; technology. @@ -48,14 +57,20 @@ Introduction - &palapeli; is a single-player jigsaw puzzle game. - The &palapeli; window is separated into two areas, the puzzle collection and the puzzle table. This manual will now continue with describing these two areas. + &palapeli; is a single-player jigsaw puzzle game. With it you can create and play puzzles ranging from 4 to 10,000 pieces. This version includes facilities to assist in solving large puzzles (300 pieces or more) within the size limits of a computer screen. + The main &palapeli; window is separated into two areas, the puzzle collection and the puzzle table. + This manual continues by describing those two areas. Browsing the puzzle collection - When you launch &palapeli; for the first time, your puzzle collection is preloaded with some default puzzles that come with &palapeli;. Click on one of the puzzles to immediately start playing. Playing takes place on the puzzle table; see the section after next for how to use the puzzle table. - Apart from selecting a puzzle, the puzzle collection can be used for the following actions: + + Playing puzzles and updating the collection + When you launch &palapeli; for the first time, your puzzle collection already contains some sample puzzles that come with &palapeli;. Click on one of them to start playing immediately. Later, when you have created some of your own puzzles (see below), you can also click on them to start playing. + Play takes place on the puzzle table; see the section after next for how to use the puzzle table. + You can select and highlight puzzles in the puzzle list by pressing &Ctrl; and &LMB; together. Pressing the &LMB; or &RMB; without &Ctrl; opens that puzzle for playing in the puzzle table. + On Apple OS X you need to double-click the &LMB; to begin playing. A single click will just select and highlight one entry in the puzzle list. + As well as playing a puzzle, you can use the puzzle collection for the following actions: create new puzzles from images on your disk import puzzles made by others @@ -63,19 +78,19 @@ delete puzzles you do not need any more The following section shows how to perform these tasks with the toolbar buttons. - + + Toolbar overview - You can select puzzles in the puzzle list by pressing &Ctrl; and &LMB; together. Pressing the &LMB; or &RMB; without &Ctrl; opens the specific puzzle in the puzzle table. Create new puzzle - Opens a dialog which allows you to create a puzzle from an image on your disk. See the next section for details. + Opens a dialog which allows you to create a puzzle from an image on your disk. See the next chapter for details. - Delete - Any puzzles that you have selected in the puzzle list, will be permanently deleted from your library. This action cannot be undone. + Delete puzzle + Any puzzles that you have selected in the puzzle list, will be permanently deleted from your collection. This action cannot be undone. Import from file @@ -91,17 +106,25 @@ Creating new puzzles - As described in the previous section, you can create new puzzles by clicking on the Create new puzzle button on the puzzle collection. + + The puzzle creation dialog + As described in the previous section, you can create new puzzles by clicking on the Create new puzzle button in the puzzle collection window. Clicking this button opens the puzzle creation dialog, which consists of three pages: - On the first page, select an image file which shall be sliced into pieces. Be sure to enter the correct metadata for the image: You should give attribution to the author of the image (⪚ the photographer or the painter); please respect the copyright of the image author. - On the second page, you can select the type of pieces into which the image shall be sliced. &palapeli; comes with a collection of slicers preinstalled, but third-party slicers may also be available. + On the first page, select an image file which will be sliced into pieces. Be sure to enter the correct metadata for the image: You should give attribution to the author of the image (⪚ the photographer or the painter); please respect the copyright of the image author. + On the second page, you can select the type of pieces into which the image will be sliced. &palapeli; comes with a collection of slicers preinstalled, but third-party slicers may also be available. On the final page of the dialog, the selected slicer can be customized ⪚ by choosing a different piece count or by adjusting parameters of the piece shape. The available set of options depends which slicer has been selected. This manual will continue to discuss the slicers which come with &palapeli;. If you have downloaded a third-party slicer plugin for &palapeli;, please refer to the documentation of this plugin if you have trouble installing or using it on your system. + + + Simple slicers + &palapeli; has two simple slicers, Classic jigsaw pieces and Rectangular pieces. These slicers create simple rectangular pieces either with or without connecting plugs. + Both slicers allow the piece count and the aspect ratio to be adjusted. Dragging the aspect ratio slider to the left results in tall pieces, adjusting it to the right produces wide pieces. The default selection produces approximately square pieces. + - &psc; - The &psc; produces realistical jigsaw pieces with various basic patterns: + The advanced &i18n-psc; + The &i18n-psc; produces realistic jigsaw pieces with various basic patterns: Rectangular grid @@ -124,15 +147,15 @@ This grid is available only if you have installed qvoronoi from the qhull package. qvoronoi is used to calculate irregular piece shapes from randomly placed points. - The &psc; has various parameters which control the appearance of the piece edges, especially the plugs on them. Default settings are provided for all of these parameters. An additional preset mode is available which reduces the amount of parameters to a bare minimum. The following parameters are available usually: + The &i18n-psc; has various parameters which control the appearance of the piece edges, especially the plugs on them. Default settings are provided for all of these parameters. An additional preset mode is available which reduces the amount of parameters to a bare minimum. The following parameters are available usually: Approx. piece count - + Flipped edge percentage - The probability for each plug to be flipped. A plug is considered flipped if it points in the opposite direction as it would in a fully regular grid. On the rectangular grid, this results in pieces with 3 or 4 plugs pointing inwards resp. outwards. Position the slider at the very left to obtain the normal fully regular grid. In the middle, plug orientation is random. At the right, the grid is fully regular again, but with an "alternate" rule. This does not mean reversal of all plugs: In the alternate regular grid, for instance, each piece has four inward-pointing or four outward-pointing plugs. This setting has no effect in the irregular grid. + The probability for each plug to be flipped. A plug is considered flipped if it points in the opposite direction as it would in a fully regular grid. On the rectangular grid, this results in pieces with 3 or 4 plugs pointing inwards resp. outwards. Position the slider at the very left to obtain the normal fully regular grid. In the middle, plug orientation is random. At the right, the grid is fully regular again, but with an alternate rule. This does not mean reversal of all plugs: In the alternate regular grid, for instance, each piece has four inward-pointing or four outward-pointing plugs. This setting has no effect in the irregular grid. Edge curviness @@ -164,25 +187,63 @@ - - Legacy slicers - For historical reasons, &palapeli; also includes the slicers "Classic jigsaw pieces" and "Rectangular pieces". These slicers create simple rectangular pieces either with or without connecting plugs. Both slicers allow the piece count and the aspect ratio to be adjusted. Dragging the aspect ratio slider to the left results in tall pieces, adjusting it to the right produces wide pieces. The default selection produces approximately square pieces. - Playing on the puzzle table + + + Basic moves The puzzle table (detail) - The object of the game is to assemble the given pieces to an image. - Pieces can be freely moved by clicking and dragging them with the &LMB;. You can select multiple pieces at once by holding &Ctrl; and clicking on them, or by dragging a rubberband around them with the &LMB;. - When you put one piece near a neighboring piece, both pieces will automatically snap together (1). After having snapped together, pieces cannot be torn apart anymore. Unlike in reality, you cannot combine two pieces that are not true neighbors. - The progress bar below the puzzle table indicates your progress (2). &palapeli; will automatically save your progress after every move, so you'll normally not have to bother about saving. - With the mouse wheel, or with the slider at the bottom right (3), you can change the zoom level of the puzzle table view. By clicking and dragging with the &RMB;, you can move the puzzle table view. + The object of the game is to assemble the given pieces into an image. + The floating Preview window shows you what that image should be. Use a toolbar or menu action to show or hide the window. For a detailed description, see The Preview window. + If you wish to attempt a puzzle that has 300 or more pieces, please read the chapter on Facilities for solving large puzzles, after you have read this chapter. + You can move a single piece by clicking and dragging it with the &LMB;. That will leave the piece selected and highlighted. To move several pieces at once, first select and highlight them, then click and drag just one of the pieces. The rest will follow. + You can select and highlight multiple pieces by holding &Ctrl; and clicking on them, or by dragging a rubberband around them with the &LMB;. You can also use &Ctrl; and &LMB; to increase or decrease the current selection one piece at a time. + To remove selection and highlighting, just click on an empty spot on the puzzle table or on a piece that is not already selected. + When you put one piece near a neighboring piece, both pieces will automatically snap together and become joined (1). After pieces have been snapped together, they cannot be torn apart. Another difference between &palapeli; and a real-world jigsaw puzzle is that you cannot combine two pieces that are not true neighbors. + The progress bar below the puzzle table indicates your progress by counting joined pieces (2). &palapeli; will automatically save your progress after every move, so you do not have to worry about saving. + You can zoom the puzzle table view in or out with the mouse wheel, the slider at the bottom right or the buttons at the bottom right (3). + You can move the puzzle table view in any direction by clicking and dragging with the &RMB;. The movement of the pieces is limited by the puzzle table area. This area is represented on the puzzle table's background by a light rectangular shade. When you move pieces to the border (4) of the puzzle table area, the area will automatically expand to give you more space for moving your puzzle pieces. Near the shaded borders, the mouse cursor changes into a double-headed arrow, indicating that you can adjust the puzzle table area by clicking there and dragging with the &LMB;. - You can lock the puzzle table area with the small button (5) right next to the progress bar. The shade (4) around the puzzle table will darken, and pieces will now stop to move at the edges of the puzzle table area. It is still possible to adjust the size of the puzzle table area manually. + You can lock the puzzle table area with the small button (5) to the right of the progress bar. The shade (4) around the puzzle table will darken, and pieces will now stop moving at the edges of the puzzle table area. It is still possible to adjust the size of the puzzle table area manually. + + + + The Preview window + The Preview is a small window that floats + above or to one side of the puzzle table window. It + contains an image of the completed puzzle: the picture + that would appear on top of the box if this was a + real-world jigsaw puzzle. + + The window can be shown or hidden by using the + Preview toolbar button or the + View + Preview menu item. Its + size, position and visible or not visible setting + are saved and restored between runs of &palapeli;. + + Being a window, it can be freely moved around + the desktop and resized. Its Close button will + hide it and its Maximize button will expand or + contract the picture rapidly. + + The Preview window has a magnification + feature, so it need not take up much space. + If you move the mouse over the window, + it zooms in quickly to reveal a magnified view + of the area under the mouse and the equivalent + of a few pieces around it. This is very useful + for viewing details of the puzzle and identifying + pieces you have found, but it can be tricky to + control, especially near the edges of the picture. + + + Mouse interactions As described in the previous section, there are plenty of ways to interact with &palapeli;'s puzzle table using the mouse. You can configure freely which actions are triggered by which mouse buttons. (See Game Configuration section for more information on how to configure this.) The following list summarizes all possible mouse actions: @@ -193,19 +254,35 @@ Move pieces by dragging - When you click and drag a puzzle piece, this piece will be moved. If multiple pieces are selected and you click and drag one of these pieces, then all pieces are moved. By default, this action is assigned to the &LMB;. + When you click and drag a puzzle piece, this piece will be moved. If multiple pieces have been selected and you click and drag one of these pieces, then all pieces are moved. By default, this action is assigned to the &LMB;. Move viewport by dragging - By default, this action is assigned to the &RMB;. + This action moves the view of the entire puzzle table in any direction. By default, it is assigned to the &RMB;. - Select multiple pieces at once + Select several pieces by rubberbanding When you click on a free area of the puzzle table instead of a puzzle piece, you can drag a rubberband. When you let the mouse button go, all pieces inside the rubberband are selected. All other pieces are deselected. By default, this action is assigned to the &LMB;. - Select pieces by clicking - When you click on a single puzzle piece, it will be selected. If it was already selected, it will be deselected. By default, this action is assigned to the &LMB; and only available when &Ctrl; is pressed. + Select several pieces by clicking + When you click on a puzzle piece, it will be selected or, if it was already selected, it will be deselected. It can also be used to adjust a rubberband selection. By default, this action is assigned to the &LMB; and only available when &Ctrl; is pressed. + + + Clear selections and highlighting + Click on an empty spot in the puzzle table or on a piece that is not already selected. + + + Zoom the puzzle table view in or out + This action is identical to clicking the buttons either side of the slider (3) on the status bar. It is assigned to the mouse wheel by default. + + + Switch to close-up or distant view + This action alternates (toggles) between a close-up and distant view, at the position of the mouse pointer. It is intended mainly to provide fast zooming in large puzzles (300 pieces or more). For more detail, see Fast zooming between close-up and distant view. By default, the action is assigned to the &MMB;. + + + Teleport pieces to or from a holder + This action helps you collect, sort and move pieces rapidly in large puzzles (300 pieces or more) without dragging and dropping. You can use it in smaller puzzles if you wish. For more detail, see Using piece-holders. By default, the action is assigned to the &LMB; and only available when &Shift; is pressed. Toggle lock state of the puzzle table area @@ -215,27 +292,474 @@ Scroll viewport horizontally and Scroll viewport vertically By default, this action is not assigned to the mouse wheel, but you can enable it from the configuration dialog. - - Zoom viewport - This action is identical to moving the slider (3) on the status bar. This action is assigned to the mouse wheel by default. - + Toolbar overview - Restart puzzle - Discards the saved progress for this puzzle. + Back to collection + Go back to the collection to choose another puzzle. Can be used only when you are solving a puzzle. + + + Preview + Toggle the display of the preview window with the completed puzzle. + + Facilities for solving large puzzles + + + General principles + &palapeli; can handle large puzzles from 300 to + 10,000 pieces, but you need some special facilities to + help you work with them on a small screen. + + You can create a large puzzle in exactly the + same way as a smaller puzzle. See the chapter on + Creating new + puzzles, but there are some things you + need to think about before you begin. For details see + Creating large + puzzles - special concerns. + + The Preview window is very useful in large + puzzles: see the section about it for a + + detailed description. + + Managing space and moving around are your next + concerns. When you zoom out to view the whole puzzle + table, it becomes hard to see individual pieces. If + you zoom in and find a piece you are looking for, it + is unlikely that the place where it goes will be + visible in the view. + See the Managing + space and moving around section for some + solutions to these problems and others. + + Finally, you need to collect pieces and convey + them to your solution area in some more efficient + way than repetitive dragging and dropping over + large distances. This is where piece-holders come in. + + When you start a large puzzle, you are given + a holder (a small floating window) called Hand + into which you can put pieces when and wherever + you see them. Then you can drop them near your + solution and work out where each piece has to go. + This is like collecting pieces in one hand as you + scan a large real-world jigsaw puzzle. + + You may need some systematic way to collect + and sort pieces by common attributes (⪚ edges, sky, + skyline, etc.) and so you can use as many holders + as you like and give them names. Holders are saved + continually, along with the puzzle table, which + means you can use them to build up parts of the + solution and store them or even to store pieces + that you wish to put into a too hard basket + until later. For details on how to use holders + see the Using + piece-holders section. + + Finally, the Mouse interactions + section lists the two special mouse actions that + make working with large puzzles quicker and easier: + teleporting pieces and zooming to close-up or + distant view. + + + + + Creating large puzzles - special concerns + Before you create a large puzzle, you should make + sure that the image in your source contains enough + detail (&ie; enough megapixels). You can use a file + utility or photo viewer/editor to find out how many + pixels wide and high your picture is when not compressed + by JPEG or other image format. As a rough rule of thumb, + allow one megapixel per 1,000 pieces. That will give + each piece about 1,000 pixels or more than 30 by 30 + pixels, making the pieces + reasonably sharp at the edges and clear to see when you + zoom in. You can work with fewer megapixels, but it may + be hard to distinguish and recognize pieces during play. + + When you create or restart a large puzzle, it may + be best to avoid using the bevel and shadowing features + of &palapeli; (see the + General + settings section), because they make loading + slower and highlighting harder to see when the pieces + in the view are very small. More prominent highlighting + appears when shadowing is not used. + + + + + Managing space and moving around + + Allocating space on the puzzle table + When you create or restart a puzzle, the + pieces are shuffled and placed randomly into grid + locations on the puzzle table. Two settings affect + the space required, see + Game Configuration + for a list of settings provided via + Settings + Configure &palapeli;... + or, on Apple OS X, + &palapeli; + Preferences.... + + The spacing of pieces in puzzle grids can be + set from 1.0 to 1.5 times the height and width of + the highest and widest pieces. Smaller settings + pack the pieces into the view better, but larger + ones allow more space for dragging, dropping and + rubberbanding. The default is 1.3, but 1.1 is + very workable in large puzzles. The setting + applies to puzzles of all sizes, and also + affects grids used with piece-holders (see + Using + piece-holders) or when re-arranging + pieces automatically on the puzzle table, using + R + MoveRearrange + pieces. + + The other setting provides space on the + puzzle table of exactly the right size for + building your solution. The default is for it + to appear in the center, with the pieces + distributed evenly around the outside. On + average, this should put the pieces closest to + where they have to go eventually. You can also + choose to use any of the four corners or have + no space (option None), in which case you have + to clear space manually, perhaps by expanding + the puzzle table area (see + Basic moves). + The solution space setting affects puzzles + down to 20 pieces, but with fewer than 20 pieces + solution spaces are pointless. + + As the puzzle solution progresses, pieces + move into the solution area and leave spaces + elsewhere. It may be helpful to pack the + remaining pieces closer together. If so, select + some pieces using rubberbanding or &Ctrl; and + &LMB;, then use + R + Move + Rearrange pieces + or simply the shortcut key (default + R). The pieces are packed into + a grid and remain selected, so that they can be + easily moved to a better position, if required. + This is also a way to gather together pieces + with some common attribute, but using + piece-holders is more powerful. + + + + Fast zooming between close-up and distant view + When a puzzle is loaded, &palapeli; + calculates distant and close-up views and + displays the distant view, which shows the + entire puzzle table area. The close-up view + shows pieces at a convenient viewing size for + your eyes and your screen and desktop. Use + the &MMB; to switch quickly between the two + views, at a location where the mouse is + pointing. Then you can home in quickly on any + piece in the puzzle table and see what shape + it is and what part of the picture it contains. + + You can adjust the scale of either view by + zooming manually and your setting will be + remembered next time you switch the view by + using the &MMB;. Puzzles of all sizes have the + fast zooming feature, but in puzzles with less + than 100 pieces the two views are almost the + same as each other. In other words, the pieces + are easy to see when you view the whole puzzle table. + + + + Moving around in a large puzzle + The hard way to move pieces in a large puzzle + is to select them and then alternately drag the + pieces and the puzzle table (&LMB; and &RMB;) + until you get to your destination. A much easier + way to do the same thing is to select the pieces, + zoom to distant view (&MMB;, see above), drag + the pieces to the destination in one move (&LMB;) + and then zoom to close-up view (&MMB;). This is + also a good way to retrieve a single stray piece, + but dragging that tiny piece across the puzzle + table, without losing the selection, can be + tricky if there are thousands of pieces. + + Another way to navigate and search the puzzle + table systematically is to zoom in on the top + left corner (&MMB;), then use the empty spaces + in the scroll bars to step through the table + horizontally or vertically, one fixed-size + page at a time. This technique is very + effective when you are using piece-holders to + collect pieces you are looking for. If you + keep the close-up scale fixed and always start + at the same place, you will always get pages + of fixed size and contents. + + + + Visibility of pieces in large puzzles + In large jigsaw puzzles on a small screen it + can be hard to see what you are doing. For + example, with a 10,000 piece puzzle on a + 1440 by 900 screen, the pieces in the distant + view of the puzzle table are about 7 pixels + across. At this scale, it is hard to see picture, + color, piece-shape or even highlighting. + + &palapeli; has always had a choice of + backgrounds and background colors and that + helps visibility. Added to these is a more + prominent highlighting scheme, which appears + if you do not choose shadowing, and a setting + to choose the color of the highlight, to + contrast with the background and most pieces. + Also these settings are now saved and restored + with each puzzle. So each puzzle can have the + background and highlighting that best suits + its overall picture. It is worth experimenting + with settings when you create a large puzzle, + but you may need to reload or restart the + puzzle before all changes can take effect. + + + + + + Using piece-holders + + What are piece-holders? + Piece-holders are perhaps the most useful + feature of &palapeli; for helping solve large + puzzles. They are small, floating windows into + which you can sort groups of pieces, such as + edges, sky, + skyline or white house on + left. Initially they appear at a minimum size + which is just large enough to show four pieces at + close-up scale, so they are quite easy to fit + in next to the main puzzle table window. + + Being windows, holders can be freely moved + around the desktop and resized. The Close button + will delete the holder, but only if it contains + no pieces. The Maximize button will expand or + contract the holder rapidly. The holder can contain + any number of pieces (maybe hundreds). A Maximize + action can be useful when reviewing the contents or + constructing a sub-assembly inside the holder. + + You can have as many holders as you like. + There is usually one which is active - outlined + in blue. It is the target for actions such as + transferring pieces or deleting a holder. + + + + Creating and deleting holders + When you start a large puzzle, a holder + called Hand is created automatically. + This may be the only holder you need with puzzles + of 300 to 750 pieces, but you can have as many + holders as you like, even with puzzles of less + than 300 pieces. + + To create or delete a holder, use + C + Move + Create piece holder... + or + D + Move + Delete piece holder, or + simply the shortcut keys C and + D, or the Close button to delete. + When you create a holder, you are asked to give it + a name, but that is not essential. Usually the + holder is at close-up scale and you can easily see + what kind of piece is in it. + + When you delete a holder, it must be active + (outlined in blue) and empty of pieces. If the + holder to be deleted is not active, click on it. + After deletion, there is no active holder, so + click on one of those that are left. + + + + Transferring pieces into and out of holders + Transferring pieces by dragging and dropping + is not possible with holders, nor is it needed. + Instead, you can transfer pieces instantly by + teleporting, as in Star Trek. + Teleporting works + by using the &Shift; and &LMB; together. + + To transfer pieces to a holder, make sure your + target holder is active (outlined in blue), + then select some pieces on the puzzle table + and click with &Shift; and &LMB;. Or you can + just click on one piece at a time with &Shift; + and &LMB;. The transferred pieces vanish from + the puzzle table and reappear, highlighted, + in the holder. As they arrive, they are + automatically packed into a grid and the last + piece transferred is centered in the view. + + To transfer pieces out of a holder, select + a piece or pieces inside the holder, then + return to the puzzle table and click on an + empty spot where you would like the piece(s) + to appear, using &Shift; and &LMB;. Avoid + clicking on a piece that is already on the + puzzle table: it will send the piece to the + the holder - not what you want. If you accidentally + do this with a large joined-up piece + (&ie; part of your solution), there + is a warning message and an opportunity to cancel. + + Pieces stay highlighted after teleporting. + That allows you to reverse the action quickly + if you accidentally transfer the wrong pieces + or use the wrong holder. Also, on the puzzle + table, you can easily adjust the position of + a piece or pieces that have just arrived or move + a piece into the solution. + + Teleporting also works between holders. + Start as if you are going to transfer pieces + to the puzzle table, but then use &Shift; + and &LMB; in an empty spot in another holder. + To reverse the process, go back to the + original holder and click with &Shift; and + &LMB; again. + + + + Sorting pieces + To sort pieces into holders efficiently + as you search through a large puzzle, use the + technique for stepping through the puzzle a + page at a time, + + as described earlier, combined with + holders and teleporting to collect pieces + as you go. When you have scanned the whole puzzle + table, zoom to a close-up of your solution area and + place the pieces you have collected. Or you + can pre-assemble pieces in holders. + + + + Other holder actions + Piece-holders support all the functions + of the puzzle table (see Basic moves), except + that you can only zoom by using the scroll-wheel + or the &MMB;. + + This means that you can use a holder as an + extra puzzle table, if you wish. You can drag + and drop, join pieces together, move the + boundaries of the area, &etc; You can also + teleport assembled pieces of any size into + or out of a holder, but beware of overlaps + or small pieces being hidden. + + Finally, you can use + AMove + Select all in holder + , followed by + RMove + Rearrange pieces + or &Shift; and &LMB;, to re-pack all + the pieces in the holder or empty all the pieces + out onto the puzzle table. Or just use the shortcut + keys A or R on the + currently selected holder. + + + + + Interface Overview - &palapeli;'s two areas, the puzzle collection and the puzzle table, are embedded into the menubar as tabs. There are also menus with standard actions which are described below. + The following are &palapeli;'s menu actions, some of which are also available as toolbar buttons and shortcut keys. + + &Ctrl;NGameCreate new puzzle... + Opens a dialog which allows you to create a puzzle from an image on your disk. See the corresponding section for details and also special concerns, if you are going to create a puzzle with 300 or more pieces. + + + GameImport from file... + When you have received a &palapeli; puzzle file (which can usually be recognized by the file extension *.puzzle), you can use this action to import it into your local collection. Puzzle files can also be played by simply clicking on them in the file manager of your choice, but after the import, the puzzle will appear in your local collection, and you can safely delete the downloaded puzzle file. + + + GameExport to file... + This will export the currently selected puzzles to files, in order to enable you to give them to your friends, publish them on the web, or make backup copies of your collection. + + + GameDelete puzzle + Any puzzles that you have selected in the puzzle list, will be permanently deleted from your collection.This action cannot be undone. + + + GameRestart puzzle + Re-shuffle all the pieces and discard the saved progress for the current puzzle. + + + CMoveCreate piece holder... + Create a temporary holder for storing or sorting pieces. See Using piece-holders for details of how to use a holder. + + + DMoveDelete piece holder + Delete a selected temporary holder when it is empty. + + + AMoveSelect all in holder + Select all pieces in the currently selected piece-holder (outlined in blue). + + + RMoveRearrange pieces + Rearrange selected pieces in any holder or the puzzle table by packing them into a grid. + + + ViewBack to collection + Go back to the collection to choose another puzzle. Can be used only when you are solving a puzzle. + + + + ViewPreview + Toggle the display of the completed puzzle preview image. Hover it with the mouse pointer to zoom in the image. + + + &Ctrl;+ViewZoom In + Increase the scale. + + + &Ctrl;-ViewZoom Out + Decrease the scale. + SettingsShow statusbar of puzzle table Toggle the display of the progress bar and buttons below the puzzle table. This action will change nothing in the puzzle collection. @@ -256,33 +780,56 @@ Game Configuration To open the configuration dialog, use the menubar option: SettingsConfigure &palapeli;... - <guilabel>General settings</guilabel> + General settings + Puzzle table + + + Lets you choose some parameters of the puzzle table. + + + Background - Lets you choose another background texture for the puzzle table. If you choose a single color instead of a texture, the button besides the selection box can be used to define this single color. + Lets you choose another background texture for the puzzle table. If you choose a single color instead of a texture, the button besides the selection box can be used to define the color. + + + Color for highlighting selected pieces + Lets you choose the highlighting color of pieces when they are selected. Click on the button to the right of the label to open the Color Selector window. + + + Space for solution + Lets you choose where to reserve some space on the puzzle table for building the solution. No space is provided if you select option None or the puzzle has less than 20 pieces. Changes will take effect when a puzzle is created or re-started. + + + Spacing of pieces in puzzle grids (1.0-1.5) + Lets you choose the spacing in puzzle grids. Changes in the overall puzzle spacing will take effect only when a puzzle is created or re-started. + + + Piece appearance - Controls the appearance of pieces on the puzzle table. If &palapeli; is running slowly, or when puzzles take very long to open, you can disable the graphical effects in this category to speed up everything. + Controls the appearance of pieces on the puzzle table. If &palapeli; is running slowly or puzzles take a long time to open, you can disable the graphical effects in this category and speed up everything. If you choose shadowing, highlighting is implemented by changing the color of the shadow. Snapping precision - Lets you choose how near neighbor pieces must be positioned to each other to snap together. If you move the slider more to the right, pieces will snap together even if they are not so close. This makes the game a bit simpler to handle. + Lets you choose how closely neighboring pieces must be positioned before they snap together. If you move the slider to the right, pieces will snap together even if they are not so close. This makes the game a bit easier to play. - <guilabel>Mouse interaction</guilabel> - This page of the configuration dialog allows you to assign the mouse interactions described in the section Playing on the puzzle table to different mouse buttons. The interactions are divided into those that can be assigned to mouse buttons (⪚ Move viewport by dragging), and those that can be assigned to the mouse wheel (⪚ Scroll viewport horizontally). + Mouse interaction + This page of the configuration dialog allows you to change the assignments of mouse buttons and modifier keys to the actions described in Playing on the puzzle table, Mouse interactions. Care should be taken to avoid ambiguous assignments. Three of the default assignments can go to the &LMB; because the pointer is over different areas when you use them, but the other assignments must be distinct. + The interactions are divided into those that can be assigned to mouse buttons (⪚ Move viewport by dragging), and those that can be assigned to the mouse wheel (⪚ Scroll viewport horizontally). - <guilabel>Mouse buttons</guilabel> tab - Next to the name of each interaction is a button with a picture of a computer mouse which shows the currently assigned action. You can configure the interaction by clicking on that button with the &LMB; and then with the mouse button which you want to assign to this interaction. If you hold modifier keys while clicking for the second time, the puzzle table will allow this interaction only while these modifiers are being held. + Mouse buttons tab + To the right of the name of each interaction is a button with a picture of a computer mouse which shows the currently assigned action. You can configure the interaction by clicking on that button with the &LMB; and then with the mouse button which you wish to assign to this interaction. If you hold modifier keys while clicking for the second time, the puzzle table will allow this interaction only while these modifiers are being held. Instead of clicking, you can also press Space to assign the special No-Button to this interaction. This is only allowed if modifier keys are being held. The No-Button means that the modifier keys take the role of the mouse button: The interaction starts when the modifier keys are pressed, and stops when one of the modifier keys is released. - <guilabel>Mouse wheel</guilabel> tab - This tab works similar to the previous one. When the button on the right asks for input, you have to turn the mouse wheel instead of clicking a mouse button. Holding modifier keys is allowed, too, with the same consequences as in the previous case. + Mouse wheel tab + This tab works similarly to the previous one. When the button on the right asks for input, you have to turn the mouse wheel instead of clicking a mouse button. Holding modifier keys is allowed, too, with the same consequences as in the previous case. If your mouse has a bidirectional mouse wheel (as most commonly found on notebook touchpads), you can take advantage of this: The button will recognize whether you turned the mouse wheel horizontally or vertically. @@ -291,8 +838,10 @@ Credits and License &palapeli; aims to bring the unagitated fun and challenge of jigsaw puzzles to your desktop. - The idea was first developed by Bernhard Schiffner, together with Stefan Majewsky, who is the current lead developer of &palapeli;. The &psc; was contributed by Johannes Löhnert. - Documentation Copyright 2009, 2010 Johannes Löhnert, Stefan Majewsky. + The idea was first developed by Bernhard Schiffner, together with Stefan Majewsky, who was the leading developer of &palapeli;. + The &i18n-psc; was contributed by Johannes Löhnert. + The large-puzzle facilities were contributed by Ian Wadham. + Documentation Copyright 2009, 2010 Johannes Löhnert, Stefan Majewsky and Copyright 2014 Ian Wadham. &underFDL; &underGPL; diff -Nru palapeli-4.12.3/src/CMakeLists.txt palapeli-4.12.90/src/CMakeLists.txt --- palapeli-4.12.3/src/CMakeLists.txt 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/CMakeLists.txt 2014-03-10 18:48:34.000000000 +0000 @@ -11,6 +11,7 @@ creator/puzzlecreator.cpp creator/slicerconfwidget.cpp creator/slicerselector.cpp + engine/gameplay.cpp engine/constraintinteractor.cpp engine/constraintvisualizer.cpp engine/interactor.cpp @@ -19,6 +20,7 @@ engine/mergegroup.cpp engine/piece.cpp engine/piecevisuals.cpp + engine/puzzlepreview.cpp engine/scene.cpp engine/texturehelper.cpp engine/trigger.cpp @@ -40,6 +42,7 @@ window/loadingwidget.cpp window/mainwindow.cpp window/puzzletablewidget.cpp + window/pieceholder.cpp ) kde4_add_kcfg_files(palapeli_SRCS settings.kcfgc) kde4_add_ui_files(palapeli_SRCS settings.ui) diff -Nru palapeli-4.12.3/src/config/configdialog.cpp palapeli-4.12.90/src/config/configdialog.cpp --- palapeli-4.12.3/src/config/configdialog.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/config/configdialog.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -63,11 +63,20 @@ //setup page "General settings" QWidget* generalPage = new QWidget; m_generalUi.setupUi(generalPage); - m_generalUi.kcfg_ViewBackground->setModel(Palapeli::TextureHelper::instance()); - addPage(generalPage, i18n("General settings"))->setIcon(KIcon( QLatin1String( "configure" ))); + + // Construction of the TextureHelper singleton also loads all Palapeli + // settings or defaults and the combobox contents for ViewBackground. + m_generalUi.kcfg_ViewBackground->setModel( + Palapeli::TextureHelper::instance()); + setupSolutionAreaComboBox(); + + addPage(generalPage, i18n("General settings"))-> + setIcon(KIcon( QLatin1String( "configure" ))); //setup page "Mouse interaction" - addPage(m_triggerPage, i18n("Mouse interaction"))->setIcon(KIcon( QLatin1String( "input-mouse" ))); - connect(m_triggerPage, SIGNAL(associationsChanged()), SLOT(updateButtons())); + addPage(m_triggerPage, i18n("Mouse interaction"))-> + setIcon(KIcon( QLatin1String( "input-mouse" ))); + connect(m_triggerPage, SIGNAL(associationsChanged()), + SLOT(updateButtons())); } bool Palapeli::ConfigDialog::hasChanged() @@ -108,6 +117,29 @@ } } +void Palapeli::ConfigDialog::setupSolutionAreaComboBox() +{ + QComboBox* b = m_generalUi.kcfg_SolutionArea; + b->insertItem(Center, i18n("Center"), Center); + b->insertItem(None, i18n("None"), None); + b->insertItem(TopLeft, i18n("Top Left"), TopLeft); + b->insertItem(TopRight, i18n("Top Right"), TopRight); + b->insertItem(BottomLeft, i18n("Bottom Left"), BottomLeft); + b->insertItem(BottomRight, i18n("Bottom Right"), BottomRight); + b->setCurrentIndex(Settings::solutionArea()); + connect(b, SIGNAL(currentIndexChanged(int)), + this, SLOT(solutionAreaChange(int))); +} + +void Palapeli::ConfigDialog::solutionAreaChange(int index) +{ + // Play safe by providing this slot. KConfigSkeleton seems to save the + // index setting if .kcfg says "Int", but it does not officially support + // the QComboBox type. + Settings::setSolutionArea(index); + Settings::self()->writeConfig(); +} + //END Palapeli::ConfigDialog #include "configdialog.moc" diff -Nru palapeli-4.12.3/src/config/configdialog.h palapeli-4.12.90/src/config/configdialog.h --- palapeli-4.12.3/src/config/configdialog.h 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/config/configdialog.h 2014-03-10 18:48:34.000000000 +0000 @@ -32,6 +32,9 @@ Q_OBJECT public: ConfigDialog(QWidget* parent = 0); + + enum SolutionSpace { Center, None, TopLeft, TopRight, + BottomLeft, BottomRight }; protected: virtual bool hasChanged(); virtual bool isDefault(); @@ -39,7 +42,10 @@ virtual void updateWidgets(); virtual void updateWidgetsDefault(); virtual void showEvent(QShowEvent* event); + private Q_SLOTS: + void solutionAreaChange(int index); private: + void setupSolutionAreaComboBox(); Ui::Settings m_generalUi; Palapeli::TriggerConfigWidget* m_triggerPage; bool m_shownForFirstTime; diff -Nru palapeli-4.12.3/src/engine/constraintinteractor.cpp palapeli-4.12.90/src/engine/constraintinteractor.cpp --- palapeli-4.12.3/src/engine/constraintinteractor.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/constraintinteractor.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -29,9 +29,14 @@ QList Palapeli::ConstraintInteractor::touchingSides(const QPointF& scenePos) const { - const QRectF sceneRect = scene()->sceneRect(); - const QSizeF handleSize = sceneRect.size() / 20; QList result; + // The scene must be Palapeli::Scene type, to get handleWidth(). + Palapeli::Scene* scene = qobject_cast(this->scene()); + if (!scene) + return result; // No scene, so no sides touching. + const QRectF sceneRect = scene->sceneRect(); + const qreal w = scene->handleWidth(); + const QSizeF handleSize = QSizeF(w, w); if ((scenePos.x() > sceneRect.left()) && (scenePos.x() < sceneRect.left() + handleSize.width())) result << LeftSide; else if ((scenePos.x() < sceneRect.right()) && (scenePos.x() > sceneRect.right() - handleSize.width())) diff -Nru palapeli-4.12.3/src/engine/constraintvisualizer.cpp palapeli-4.12.90/src/engine/constraintvisualizer.cpp --- palapeli-4.12.3/src/engine/constraintvisualizer.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/constraintvisualizer.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -21,6 +21,7 @@ #include #include +#include // IDW test. Palapeli::ConstraintVisualizer::ConstraintVisualizer(Palapeli::Scene* scene) : m_scene(scene) @@ -28,20 +29,27 @@ , m_shadowItems(SideCount) , m_handleItems(HandleCount) , m_animator(new QPropertyAnimation(this, "opacity", this)) + , m_sceneRect(QRectF()) + , m_thickness(5.0) + , m_isStopped(true) { + // All QGraphicsRectItems have null size until the first update(). setOpacity(0.3); - //create shadow items (with null size until first update()) + // Create shadow items. These are outside the puzzle table. QColor rectColor(Qt::black); - rectColor.setAlpha(80); + // IDW test. rectColor.setAlpha(80); + rectColor.setAlpha(40); // Outer area is paler. for (int i = 0; i < SideCount; ++i) { m_shadowItems[i] = new QGraphicsRectItem(this); m_shadowItems[i]->setPen(Qt::NoPen); m_shadowItems[i]->setBrush(rectColor); } - //create handle items (also with null size) - rectColor.setAlpha(rectColor.alpha() / 2); - Qt::CursorShape shapes[] = { Qt::SizeHorCursor, Qt::SizeFDiagCursor, Qt::SizeVerCursor, Qt::SizeBDiagCursor }; + // Create handle items. These are the edges and corners of the table. + // IDW test. rectColor.setAlpha(rectColor.alpha() / 2); + rectColor.setAlpha(rectColor.alpha() * 2); // Table edge is darker. + Qt::CursorShape shapes[] = { Qt::SizeHorCursor, Qt::SizeFDiagCursor, + Qt::SizeVerCursor, Qt::SizeBDiagCursor }; for (int i = 0; i < HandleCount; ++i) { m_handleItems[i] = new QGraphicsRectItem(this); @@ -51,14 +59,40 @@ } //more initialization QObject::setParent(scene); //delete myself automatically when the scene is destroyed - scene->addItem(this); +} + +void Palapeli::ConstraintVisualizer::start (const QRectF& sceneRect, + const int thickness) +{ + // Puzzle loading nearly finished: add resize handles and shadow areas. + if (!m_isStopped) { + return; // Duplicate call. + } + m_thickness = thickness; + this->update(sceneRect); + m_scene->addItem(this); setZValue(-1); - //NOTE: The QueuedConnection is necessary because setSceneRect() sends out - //the sceneRectChanged() signal before it disables automatic growing of the - //scene rect. If the connection was direct, we could thus enter an infinite - //loop when the constraint visualizer enlarges itself in reaction to the - //changed sceneRect, thereby changing the autogrowing sceneRect again. - connect(scene, SIGNAL(sceneRectChanged(QRectF)), this, SLOT(update(QRectF)), Qt::QueuedConnection); + + // NOTE: The QueuedConnection is necessary because setSceneRect() sends + // out the sceneRectChanged() signal before it disables automatic + // growing of the scene rect. If the connection was direct, we could + // thus enter an infinite loop when the constraint visualizer enlarges + // itself in reaction to the changed sceneRect, thereby changing the + // autogrowing sceneRect again. + connect(m_scene, SIGNAL(sceneRectChanged(QRectF)), + this, SLOT(update(QRectF)), Qt::QueuedConnection); + m_isStopped = false; +} + +void Palapeli::ConstraintVisualizer::stop() +{ + if (m_isStopped) { + return; // Starting first loadPuzzle(): nothing to do. + } + m_scene->removeItem(this); + disconnect(m_scene, SIGNAL(sceneRectChanged(QRectF))); + m_sceneRect = QRectF(); + m_isStopped = true; } bool Palapeli::ConstraintVisualizer::isActive() const @@ -82,56 +116,73 @@ { if (m_sceneRect == sceneRect) return; + // Make sure the ConstraintVisualizer stays outside the pieces' area. + QRectF minimumRect = m_scene->piecesBoundingRect(); + qreal m = m_scene->margin(); + minimumRect.adjust(-m, -m, m, m); m_sceneRect = sceneRect; - const QSizeF handleSize = sceneRect.size() / 20; - //find a fictional viewport rect which we want to cover (except for the contained scene rect) + if(!sceneRect.contains(minimumRect)) { + // IDW TODO - Works and seems safe, + // but it may be better for interactor to check. + m_sceneRect = minimumRect; + m_scene->setSceneRect(minimumRect); + } + // Find a fictional viewport we want to cover (except for scene rect). const qreal viewportRectSizeFactor = 10; - QRectF viewportRect = sceneRect; - viewportRect.setSize(viewportRectSizeFactor * sceneRect.size()); - viewportRect.moveCenter(sceneRect.center()); + QRectF viewportRect = m_sceneRect; + viewportRect.setSize(viewportRectSizeFactor * m_sceneRect.size()); + viewportRect.moveCenter(m_sceneRect.center()); + // The shadow areas are the areas outside the puzzle table. //adjust left shadow area QRectF itemRect = viewportRect; - itemRect.setRight(sceneRect.left()); + itemRect.setRight(m_sceneRect.left()); m_shadowItems[LeftSide]->setRect(itemRect); //adjust right shadow area itemRect = viewportRect; - itemRect.setLeft(sceneRect.right()); + itemRect.setLeft(m_sceneRect.right()); m_shadowItems[RightSide]->setRect(itemRect); //adjust top shadow area itemRect = viewportRect; - itemRect.setBottom(sceneRect.top()); - itemRect.setLeft(sceneRect.left()); //do not overlap left area... - itemRect.setRight(sceneRect.right()); //..and right area + itemRect.setBottom(m_sceneRect.top()); + itemRect.setLeft(m_sceneRect.left()); //do not overlap left area... + itemRect.setRight(m_sceneRect.right()); //..and right area m_shadowItems[TopSide]->setRect(itemRect); //adjust bottom shadow area itemRect = viewportRect; - itemRect.setTop(sceneRect.bottom()); - itemRect.setLeft(sceneRect.left()); //same as above - itemRect.setRight(sceneRect.right()); + itemRect.setTop(m_sceneRect.bottom()); + itemRect.setLeft(m_sceneRect.left()); //same as above + itemRect.setRight(m_sceneRect.right()); m_shadowItems[BottomSide]->setRect(itemRect); + // + // The handles are the draggable borders of the puzzle table. //adjust edge handles - QRectF handleRect(QPointF(), handleSize); - handleRect.moveTopLeft(sceneRect.topLeft()); + // IDW test.QRectF handleRect(QPointF(), handleSize); + QRectF handleRect(QPointF(), QSizeF(m_thickness, m_thickness)); + handleRect.moveTopLeft(m_sceneRect.topLeft()); m_handleItems[TopLeftHandle]->setRect(handleRect); - handleRect.moveTopRight(sceneRect.topRight()); + handleRect.moveTopRight(m_sceneRect.topRight()); m_handleItems[TopRightHandle]->setRect(handleRect); - handleRect.moveBottomLeft(sceneRect.bottomLeft()); + handleRect.moveBottomLeft(m_sceneRect.bottomLeft()); m_handleItems[BottomLeftHandle]->setRect(handleRect); - handleRect.moveBottomRight(sceneRect.bottomRight()); + handleRect.moveBottomRight(m_sceneRect.bottomRight()); m_handleItems[BottomRightHandle]->setRect(handleRect); //adjust top/bottom handles - handleRect.setSize(QSizeF(sceneRect.width() - 2 * handleSize.width(), handleSize.height())); - handleRect.moveCenter(sceneRect.center()); - handleRect.moveTop(sceneRect.top()); + // IDW test. handleRect.setSize(QSizeF(m_sceneRect.width() - 2 * handleSize.width(), handleSize.height())); + handleRect.setSize(QSizeF(m_sceneRect.width() - 2 * m_thickness, + m_thickness)); + handleRect.moveCenter(m_sceneRect.center()); + handleRect.moveTop(m_sceneRect.top()); m_handleItems[TopHandle]->setRect(handleRect); - handleRect.moveBottom(sceneRect.bottom()); + handleRect.moveBottom(m_sceneRect.bottom()); m_handleItems[BottomHandle]->setRect(handleRect); //adjust left/right handles - handleRect.setSize(QSizeF(handleSize.width(), sceneRect.height() - 2 * handleSize.height())); - handleRect.moveCenter(sceneRect.center()); - handleRect.moveLeft(sceneRect.left()); + // IDW test. handleRect.setSize(QSizeF(handleSize.width(), m_sceneRect.height() - 2 * handleSize.height())); + handleRect.setSize(QSizeF(m_thickness, + m_sceneRect.height() - 2 * m_thickness)); + handleRect.moveCenter(m_sceneRect.center()); + handleRect.moveLeft(m_sceneRect.left()); m_handleItems[LeftHandle]->setRect(handleRect); - handleRect.moveRight(sceneRect.right()); + handleRect.moveRight(m_sceneRect.right()); m_handleItems[RightHandle]->setRect(handleRect); } diff -Nru palapeli-4.12.3/src/engine/constraintvisualizer.h palapeli-4.12.90/src/engine/constraintvisualizer.h --- palapeli-4.12.3/src/engine/constraintvisualizer.h 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/constraintvisualizer.h 2014-03-10 18:48:34.000000000 +0000 @@ -29,19 +29,41 @@ class CvHandleItem; class Scene; - class ConstraintVisualizer : public Palapeli::GraphicsObject + /* This class creates and manages a draggable frame around a Palapeli + * scene, which is used to expand or contract the scene. The principal + * Palapeli::Scene object is the puzzle table. When a puzzle is loaded, + * the puzzle table and other scenes have their frames (i.e. constraint + * visualizers) automatically set to surround the pieces closely. + * + * A scene can be locked by the user, via Scene::setConstrained(), in + * which case the frame and surrounding areas darken and the frame + * cannot be moved by pushing pieces against it, but the scene size + * can still be changed by dragging the frame with the mouse. + * + * The scene is then said to be "constrained" and its constraint + * visualizer object is "active" (ConstraintVisualizer::m_active true). + */ + class ConstraintVisualizer : public + Palapeli::GraphicsObject { Q_OBJECT public: ConstraintVisualizer(Palapeli::Scene* scene); bool isActive() const; + void start (const QRectF& sceneRect, + const int thickness); + void stop(); public Q_SLOTS: void setActive(bool active); void update(const QRectF& sceneRect); private: - enum Side { LeftSide = 0, RightSide, TopSide, BottomSide, SideCount }; - enum HandlePosition { LeftHandle = 0, TopLeftHandle, TopHandle, TopRightHandle, RightHandle, BottomRightHandle, BottomHandle, BottomLeftHandle, HandleCount }; + enum Side { LeftSide = 0, RightSide, + TopSide, BottomSide, SideCount }; + enum HandlePosition { LeftHandle = 0, TopLeftHandle, + TopHandle, TopRightHandle, RightHandle, + BottomRightHandle, BottomHandle, + BottomLeftHandle, HandleCount }; Palapeli::Scene* m_scene; bool m_active; @@ -50,6 +72,8 @@ QGraphicsPathItem* m_indicatorItem; QRectF m_sceneRect; QPropertyAnimation* m_animator; + bool m_isStopped; + qreal m_thickness; }; } diff -Nru palapeli-4.12.3/src/engine/gameplay.cpp palapeli-4.12.90/src/engine/gameplay.cpp --- palapeli-4.12.3/src/engine/gameplay.cpp 1970-01-01 00:00:00.000000000 +0000 +++ palapeli-4.12.90/src/engine/gameplay.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -0,0 +1,1289 @@ +/*************************************************************************** + * Copyright 2009 Stefan Majewsky + * Copyright 2014 Ian Wadham + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +***************************************************************************/ + +#include "gameplay.h" + +#include "../file-io/collection-view.h" +#include "../window/puzzletablewidget.h" +#include "../window/pieceholder.h" +#include "puzzlepreview.h" + +#include "scene.h" +#include "view.h" +#include "piece.h" +#include "texturehelper.h" +#include "../file-io/puzzle.h" +#include "../file-io/components.h" +#include "../file-io/collection.h" +#include "../creator/puzzlecreator.h" + +#include "../config/configdialog.h" +#include "settings.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Use this because comma in type is not possible in foreach macro. +typedef QPair DoubleIntPair; + +//TODO: move LoadingWidget into here (stack into m_centralWidget) + +const int Palapeli::GamePlay::LargePuzzle = 300; + +const QString HeaderSaveGroup ("-PalapeliSavedPuzzle"); +const QString HolderSaveGroup ("Holders"); +const QString LocationSaveGroup ("XYCo-ordinates"); +const QString FormerSaveGroup ("SaveGame"); +const QString AppearanceSaveGroup("Appearance"); +const QString PreviewSaveGroup ("PuzzlePreview"); + +Palapeli::GamePlay::GamePlay(MainWindow* mainWindow) + : QObject(mainWindow) + , m_centralWidget(new QStackedWidget) + , m_collectionView(new Palapeli::CollectionView) + , m_puzzleTable(new Palapeli::PuzzleTableWidget) + , m_puzzlePreview(0) + , m_mainWindow(mainWindow) + , m_puzzle(0) + , m_pieceAreaSize(QSizeF(32.0, 32.0)) // Allow 1024 pixels initially. + , m_savegameTimer(new QTimer(this)) + , m_loadingPuzzle(false) + , m_restoredGame(false) + , m_originalPieceCount(0) + , m_currentPieceCount(0) + , m_sizeFactor(1.3) + , m_currentHolder(0) + , m_previousHolder(0) + , m_playing(false) + , m_canDeletePuzzle(false) // No puzzle selected at startup. + , m_canExportPuzzle(false) +{ + m_puzzleTableScene = m_puzzleTable->view()->scene(); + m_viewList << m_puzzleTable->view(); + m_savegameTimer->setInterval(500); //write savegame twice per second at most + m_savegameTimer->setSingleShot(true); + connect(m_savegameTimer, SIGNAL(timeout()), this, SLOT(updateSavedGame())); + connect(this, SIGNAL(reportProgress(int,int)), + m_puzzleTable, SLOT(reportProgress(int,int))); + connect(this, SIGNAL(victoryAnimationFinished()), + m_puzzleTable->view(), SLOT(startVictoryAnimation())); + connect(m_puzzleTable->view(), + SIGNAL(teleport(Piece*,const QPointF&,View*)), + this, + SLOT(teleport(Piece*,const QPointF&,View*))); +} + +Palapeli::GamePlay::~GamePlay() +{ + deletePuzzleViews(); + delete m_puzzlePreview; +} + +void Palapeli::GamePlay::deletePuzzleViews() +{ + qDebug() << "ENTERED GamePlay::deletePuzzleViews() ..."; + while (! m_viewList.isEmpty()) { + Palapeli::View* view = m_viewList.takeLast(); + Palapeli::Scene* scene = view->scene(); + qDebug() << "DISCONNECT SLOT(positionChanged(int))"; + disconnect(scene, SIGNAL(saveMove(int)), + this, SLOT(positionChanged(int))); + qDebug() << "scene->clearPieces();"; + scene->clearPieces(); + qDebug() << "if (scene != m_puzzleTableScene) {"; + if (scene != m_puzzleTableScene) { + qDebug() << "DELETING holder" << view->windowTitle(); + delete view; + } + } +} + +void Palapeli::GamePlay::init() +{ + // Set up the collection view. + m_collectionView->setModel(Palapeli::Collection::instance()); + connect(m_collectionView, SIGNAL(playRequest(Palapeli::Puzzle*)), SLOT(playPuzzle(Palapeli::Puzzle*))); + + // Set up the puzzle table. + m_puzzleTable->showStatusBar(Settings::showStatusBar()); + + // Set up the central widget. + m_centralWidget->addWidget(m_collectionView); + m_centralWidget->addWidget(m_puzzleTable); + m_centralWidget->setCurrentWidget(m_collectionView); + m_mainWindow->setCentralWidget(m_centralWidget); + // Get some current action states from the collection. + m_canDeletePuzzle = m_mainWindow->actionCollection()-> + action("game_delete")->isEnabled(); + m_canExportPuzzle = m_mainWindow->actionCollection()-> + action("game_export")->isEnabled(); + // Enable collection actions and disable playing actions initially. + setPalapeliMode(false); +} + +void Palapeli::GamePlay::shutdown() +{ + qDebug() << "ENTERED Palapeli::GamePlay::shutdown()"; + // Make sure the last change is saved. + if (m_savegameTimer->isActive()) { + m_savegameTimer->stop(); + updateSavedGame(); + } + // Delete piece-holders cleanly: no closeEvents in PieceHolder objects + // and no messages about holders not being empty. + deletePuzzleViews(); +} + +//BEGIN action handlers + +void Palapeli::GamePlay::playPuzzle(Palapeli::Puzzle* puzzle) +{ + t.start(); // IDW test. START the clock. + qDebug() << "START playPuzzle(): elapsed 0"; + // Get some current action states from the collection. + m_canDeletePuzzle = m_mainWindow->actionCollection()-> + action("game_delete")->isEnabled(); + m_canExportPuzzle = m_mainWindow->actionCollection()-> + action("game_export")->isEnabled(); + m_centralWidget->setCurrentWidget(m_puzzleTable); + m_puzzlePreview = new Palapeli::PuzzlePreview(m_mainWindow); + + if (m_loadingPuzzle || (!puzzle) || (m_puzzle == puzzle)) { + if (m_puzzle == puzzle) { + qDebug() << "RESUMING A PUZZLE."; + // IDW TODO - Show piece-holders. + // Check if puzzle has been completed. + if (m_currentPieceCount == 1) { + int result = KMessageBox::questionYesNo( + m_mainWindow, + i18n("You have finished the puzzle. Do you want to restart it now?")); + if (result == KMessageBox::Yes) { + restartPuzzle(); + return; + } + } + // True if same puzzle selected and not still loading. + setPalapeliMode(! m_loadingPuzzle); + } + qDebug() << "NO LOAD: (m_puzzle == puzzle)" + << (m_puzzle == puzzle); + qDebug() << "m_loadingPuzzle" << m_loadingPuzzle + << (puzzle ? "puzzle != 0" : "puzzle == 0"); + return; // Already loaded, loading or failed to start. + } + m_puzzle = puzzle; + qDebug() << "RESTART the clock: elapsed" << t.restart(); // IDW test. + loadPuzzle(); + qDebug() << "Returned from loadPuzzle(): elapsed" << t.elapsed(); + + // IDW TODO - There is no way to stop loading a puzzle and start loading + // another. The only option is to Quit or abort Palapeli. + + QTimer::singleShot(0, this, SLOT(loadPreview())); +} + +void Palapeli::GamePlay::loadPreview() +{ + // IDW TODO - This WAS delaying the showing of the LoadingWidget. Now + // it is preventing the balls from moving for a few seconds. + + // Get metadata from archive (tar), to be sure of getting image data. + // The config/palapeli-collectionrc file lacks image metadata (because + // Palapeli must load the collection-list quickly at startup time). + const Palapeli::PuzzleComponent* as = + m_puzzle->get(Palapeli::PuzzleComponent::ArchiveStorage); + const Palapeli::PuzzleComponent* cmd = (as == 0) ? 0 : + as->cast(Palapeli::PuzzleComponent::Metadata); + if (cmd) { + // Load puzzle preview image from metadata. + const Palapeli::PuzzleMetadata md = + dynamic_cast(cmd)-> + metadata; + m_puzzlePreview->loadImageFrom(md); + m_mainWindow->setCaption(md.name); // Set main title. + } + + m_puzzlePreview->setVisible(Settings::puzzlePreviewVisible()); + connect (m_puzzlePreview, SIGNAL(closing()), + SLOT(actionTogglePreview())); // Hide preview: do not delete. +} + +void Palapeli::GamePlay::playPuzzleFile(const QString& path) +{ + const QString id = Palapeli::Puzzle::fsIdentifier(path); + playPuzzle(new Palapeli::Puzzle(new Palapeli::ArchiveStorageComponent, + path, id)); +} + +void Palapeli::GamePlay::actionGoCollection() +{ + m_centralWidget->setCurrentWidget(m_collectionView); + delete m_puzzlePreview; + m_puzzlePreview = 0; + m_mainWindow->setCaption(QString()); + // IDW TODO - Disable piece-holder actions. + foreach (Palapeli::View* view, m_viewList) { + if (view != m_puzzleTable->view()) { + view->hide(); + } + } + // Disable playing actions and enable collection actions. + setPalapeliMode(false); +} + +void Palapeli::GamePlay::actionTogglePreview() +{ + // This action is OK during puzzle loading. + if (m_puzzlePreview) { + m_puzzlePreview->toggleVisible(); + m_mainWindow->actionCollection()->action("view_preview")-> + setChecked(Settings::puzzlePreviewVisible()); + } +} + +void Palapeli::GamePlay::actionCreate() +{ + QPointer creatorDialog(new Palapeli::PuzzleCreatorDialog); + if (creatorDialog->exec()) + { + if (!creatorDialog) + return; + Palapeli::Puzzle* puzzle = creatorDialog->result(); + if (!puzzle) { + delete creatorDialog; + return; + } + Palapeli::Collection::instance()->importPuzzle(puzzle); + playPuzzle(puzzle); + } + delete creatorDialog; +} + +void Palapeli::GamePlay::actionDelete() +{ + QModelIndexList indexes = m_collectionView->selectedIndexes(); + //ask user for confirmation + QStringList puzzleNames; + foreach (const QModelIndex& index, indexes) + puzzleNames << index.data(Qt::DisplayRole).toString(); + const int result = KMessageBox::warningContinueCancelList(m_mainWindow, i18n("The following puzzles will be deleted. This action cannot be undone."), puzzleNames); + if (result != KMessageBox::Continue) + return; + //do deletion + Palapeli::Collection* coll = Palapeli::Collection::instance(); + foreach (const QModelIndex& index, indexes) + coll->deletePuzzle(index); +} + +void Palapeli::GamePlay::actionImport() +{ + const QString filter = i18nc("Filter for a file dialog", "*.puzzle|Palapeli puzzles (*.puzzle)"); + const QStringList paths = KFileDialog::getOpenFileNames(KUrl("kfiledialog:///palapeli-import"), filter); + Palapeli::Collection* coll = Palapeli::Collection::instance(); + foreach (const QString& path, paths) + coll->importPuzzle(path); +} + +void Palapeli::GamePlay::actionExport() +{ + QModelIndexList indexes = m_collectionView->selectedIndexes(); + Palapeli::Collection* coll = Palapeli::Collection::instance(); + foreach (const QModelIndex& index, indexes) + { + Palapeli::Puzzle* puzzle = coll->puzzleFromIndex(index); + if (!puzzle) + continue; + //get puzzle name (as an initial guess for the file name) + puzzle->get(Palapeli::PuzzleComponent::Metadata).waitForFinished(); + const Palapeli::MetadataComponent* cmp = puzzle->component(); + if (!cmp) + continue; + //ask user for target file name + const QString startLoc = QString::fromLatin1("kfiledialog:///palapeli-export/%1.puzzle").arg(cmp->metadata.name); + const QString filter = i18nc("Filter for a file dialog", "*.puzzle|Palapeli puzzles (*.puzzle)"); + const QString location = KFileDialog::getSaveFileName(KUrl(startLoc), filter); + if (location.isEmpty()) + continue; //process aborted by user + //do export + coll->exportPuzzle(index, location); + } +} + +void Palapeli::GamePlay::createHolder() +{ + qDebug() << "GamePlay::createHolder() entered"; + bool OK; + QString name = QInputDialog::getText(m_mainWindow, + i18n("Create a piece holder"), + i18n("Enter a short name (optional):"), + QLineEdit::Normal, QString(""), &OK); + if (! OK) { + return; // If CANCELLED, do not create a piece holder. + } + createHolder(name); + // Merges/moves in new holders add to the progress bar and are saved. + Palapeli::View* view = m_viewList.last(); + Palapeli::PieceHolder* h = qobject_cast(view); + h->initializeZooming(); + connect(view->scene(), SIGNAL(saveMove(int)), + this, SLOT(positionChanged(int))); + connect(view, + SIGNAL(teleport(Piece*,const QPointF&,View*)), + this, + SLOT(teleport(Piece*,const QPointF&,View*))); + connect(view, SIGNAL(newPieceSelectionSeen(View*)), + this, SLOT(handleNewPieceSelection(View*))); +} + +void Palapeli::GamePlay::createHolder(const QString& name, bool sel) +{ + Palapeli::PieceHolder* h = + new Palapeli::PieceHolder(m_mainWindow, m_pieceAreaSize, name); + m_viewList << h; + connect(h, SIGNAL(selected(PieceHolder*)), + this, SLOT(changeSelectedHolder(PieceHolder*))); + connect (h, SIGNAL(closing(PieceHolder*)), + SLOT(closeHolder(PieceHolder*))); + if (sel) { + changeSelectedHolder(h); + } + else { + h->setSelected(false); + } + m_puzzleTable->view()->setFocus(Qt::OtherFocusReason); + m_puzzleTable->activateWindow(); // Return focus to main window. + positionChanged(0); // Save holder - a little later. +} + +void Palapeli::GamePlay::deleteHolder() +{ + qDebug() << "GamePlay::deleteHolder() entered"; + if (m_currentHolder) { + closeHolder(m_currentHolder); + } + else { + KMessageBox::information(m_mainWindow, + i18n("You need to click on a piece holder to " + "select it before you can delete it, or " + "you can just click on its Close button.")); + } +} + +void Palapeli::GamePlay::closeHolder(Palapeli::PieceHolder* h) +{ + if (h->scene()->pieces().isEmpty()) { + int count = m_viewList.count(); + m_viewList.removeOne(h); + qDebug() << "m_viewList WAS" << count << "NOW" << m_viewList.count(); + m_currentHolder = 0; + m_previousHolder = 0; + h->deleteLater(); + positionChanged(0); // Save change - a little later. + } + else { + KMessageBox::information(m_mainWindow, + i18n("The selected piece holder must be empty " + "before you can delete it.")); + } +} + +void Palapeli::GamePlay::selectAll() +{ + qDebug() << "GamePlay::selectAll() entered"; + if (m_currentHolder) { + QList pieces = + m_currentHolder->scene()->pieces(); + if (! pieces.isEmpty()) { + foreach (Palapeli::Piece* piece, pieces) { + piece->setSelected(true); + } + handleNewPieceSelection(m_currentHolder); + } + else { + KMessageBox::information(m_mainWindow, + i18n("The selected piece holder must contain " + "some pieces for 'Select all' to use.")); + } + } + else { + KMessageBox::information(m_mainWindow, + i18n("You need to click on a piece holder to " + "select it before you can select all the " + "pieces in it.")); + } +} + +void Palapeli::GamePlay::rearrangePieces() +{ + qDebug() << "GamePlay::rearrangePieces() entered"; + QList selectedPieces; + Palapeli::View* view = m_puzzleTable->view(); + selectedPieces = getSelectedPieces(view); + if (selectedPieces.isEmpty()) { + if (m_currentHolder) { + view = m_currentHolder; + selectedPieces = getSelectedPieces(view); + } + } + if (selectedPieces.isEmpty()) { + KMessageBox::information(m_mainWindow, + i18n("To rearrange pieces, either the puzzle table " + "must have some selected pieces or there " + "must be a selected holder with some selected " + "pieces in it.")); + return; + } + QRectF bRect; + foreach (Palapeli::Piece* piece, selectedPieces) { + bRect |= piece->sceneBareBoundingRect(); + } + Palapeli::Scene* scene = view->scene(); + scene->initializeGrid(bRect.topLeft()); + foreach (Palapeli::Piece* piece, selectedPieces) { + scene->addToGrid(piece); + } + positionChanged(0); // There is no attempt to merge pieces here. +} + +void Palapeli::GamePlay::actionZoomIn() +{ + // IDW TODO - Make ZoomIn work for whichever view is active. + m_puzzleTable->view()->zoomIn(); +} + +void Palapeli::GamePlay::actionZoomOut() +{ + // IDW TODO - Make ZoomOut work for whichever view is active. + m_puzzleTable->view()->zoomOut(); +} + +void Palapeli::GamePlay::restartPuzzle() +{ + if (!m_puzzle) { + return; // If no puzzle was successfully loaded and started. + } + // Discard the *.save file. + static const QString pathTemplate = + QString::fromLatin1("collection/%1.save"); + QFile(KStandardDirs::locateLocal("appdata", + pathTemplate.arg(m_puzzle->identifier()))).remove(); + // Load the puzzle and re-shuffle the pieces. + loadPuzzle(); +} + +void Palapeli::GamePlay::teleport(Palapeli::Piece* pieceUnderMouse, + const QPointF& scenePos, Palapeli::View* view) +{ + qDebug() << "GamePlay::teleport: pieceUnder" << (pieceUnderMouse != 0) + << "scPos" << scenePos + << "PuzzleTable?" << (view == m_puzzleTable->view()) + << "CurrentHolder?" << (view == m_currentHolder); + if (! m_currentHolder) { + KMessageBox::information(m_mainWindow, + i18n("You need to have a piece holder and click it to " + "select it before you can transfer pieces into or " + "out of it.")); + return; + } + bool puzzleTableClick = (view == m_puzzleTable->view()); + QList selectedPieces; + if (puzzleTableClick) { + if (pieceUnderMouse && (!pieceUnderMouse->isSelected())) { + pieceUnderMouse->setSelected(true); + } + selectedPieces = getSelectedPieces(view); + if (selectedPieces.count() > 0) { + // Transfer from the puzzle table to a piece-holder. + foreach (Palapeli::Piece* piece, selectedPieces) { + if (piece->representedAtomicPieces().count() + > 6) { + int ans = 0; + ans = KMessageBox::questionYesNo ( + m_mainWindow, + i18n("You have selected to " + "transfer a large piece " + "containing more than six " + "small pieces to a holder. Do " + "you really wish to do that?")); + if (ans == KMessageBox::No) { + return; + } + } + } + transferPieces(selectedPieces, view, m_currentHolder); + } + else { + selectedPieces = getSelectedPieces(m_currentHolder); + qDebug() << "Transfer from holder" << selectedPieces.count() << m_currentHolder->name(); + // Transfer from a piece-holder to the puzzle table. + if (selectedPieces.count() > 0) { + transferPieces(selectedPieces, m_currentHolder, + view, scenePos); + } + else { + KMessageBox::information(m_mainWindow, + i18n("You need to select one or more " + "pieces to be transferred out of " + "the selected holder or select " + "pieces from the puzzle table " + "to be transferred into it.")); + } + } + } + else { + if (m_previousHolder) { + selectedPieces = getSelectedPieces(m_previousHolder); + // Transfer from one piece-holder to another. + if (selectedPieces.count() > 0) { + transferPieces(selectedPieces, m_previousHolder, + view, scenePos); + } + else { + KMessageBox::information(m_mainWindow, + i18n("You need to select one or more " + "pieces to be transferred from " + "the previous holder into the " + "newly selected holder.")); + } + } + else { + KMessageBox::information(m_mainWindow, + i18n("You need to have at least two holders, " + "one of them selected and with selected " + "pieces inside it, before you can " + "transfer pieces to a second holder.")); + } + } + positionChanged(0); // Save the transfer - a little later. +} + +void Palapeli::GamePlay::handleNewPieceSelection(Palapeli::View* view) +{ + // De-select pieces on puzzle table, to prevent teleport bounce-back. + Palapeli::View* m_puzzleTableView = m_puzzleTable->view(); + if (view != m_puzzleTableView) { // Pieces selected in a holder. + foreach (Palapeli::Piece* piece, + getSelectedPieces(m_puzzleTableView)) { + piece->setSelected(false); + } + } +} + +void Palapeli::GamePlay::transferPieces(const QList pieces, + Palapeli::View* source, + Palapeli::View* dest, + const QPointF& scenePos) +{ + qDebug() << "ENTERED GamePlay::transferPieces(): pieces" << pieces.count() << "SourceIsTable" << (source == m_puzzleTable->view()) << "DestIsTable" << (dest == m_puzzleTable->view()) << "scenePos" << scenePos; + source->scene()->dispatchPieces(pieces); + if ((source != m_puzzleTable->view()) && // If empty holder. + (source->scene()->pieces().isEmpty())) { + source->scene()->initializeGrid(QPointF(0.0, 0.0)); + } + + bool destIsPuzzleTable = (dest == m_puzzleTable->view()); + if (destIsPuzzleTable) { + m_puzzleTableScene->initializeGrid(scenePos); + } + Palapeli::Scene* scene = dest->scene(); + foreach (Palapeli::Piece* piece, scene->pieces()) { + // Clear all previous selections in the destination scene. + if (piece->isSelected()) { + piece->setSelected(false); + } + } + foreach (Palapeli::Piece* piece, pieces) { + // Leave the new arrivals selected, connected and in a grid. + scene->addPieceToList(piece); + scene->addItem(piece); + scene->addToGrid(piece); + piece->setSelected(true); + connect(piece, SIGNAL(moved(bool)), + scene, SLOT(pieceMoved(bool))); + } + scene->setSceneRect(scene->piecesBoundingRect()); + if (! destIsPuzzleTable) { + dest->centerOn(pieces.last()->sceneBareBoundingRect().center()); + } +} + +void Palapeli::GamePlay::setPalapeliMode(bool playing) +{ + // Palapeli has three modes: playing, loading and managing a collection. + // When playing, collection actions are disabled and playing actions are + // enabled: vice versa when managing the collection. When loading a + // puzzle, both sets of actions are disabled, because they cannot work + // concurrently with loading (enPlaying and enCollection both false). + + const char* playingActions[] = {"view_collection", "game_restart", + "view_preview", "move_create_holder", + "move_delete_holder", "move_select_all", + "move_rearrange", "view_zoom_in", + "view_zoom_out", "END" }; + const char* collectionActions[] = {"game_new", "game_delete", + "game_import", "game_export", "END" }; + bool enPlaying = (! m_loadingPuzzle) && playing; + bool enCollection = (! m_loadingPuzzle) && (! playing); + + for (uint i = 0; (strcmp (playingActions[i], "END") != 0); i++) { + m_mainWindow->actionCollection()-> + action(playingActions[i])->setEnabled(enPlaying); + } + for (uint i = 0; (strcmp (collectionActions[i], "END") != 0); i++) { + m_mainWindow->actionCollection()-> + action(collectionActions[i])->setEnabled(enCollection); + } + // The collection view may enable or disable Delete and Export actions, + // depending on what puzzle, if any, is currently selected. + if (enCollection) { + m_mainWindow->actionCollection()-> + action("game_delete")->setEnabled(m_canDeletePuzzle); + m_mainWindow->actionCollection()-> + action("game_export")->setEnabled(m_canExportPuzzle); + } + m_playing = playing; +} + +QList Palapeli::GamePlay::getSelectedPieces(Palapeli::View* v) +{ + qDebug() << "ENTERED GamePlay::getSelectedPieces(): PuzzleTable" << (v == m_puzzleTable->view()); + const QList sel = v->scene()->selectedItems(); + QList pieces; + foreach (QGraphicsItem* item, sel) { + Palapeli::Piece* p = Palapeli::Piece::fromSelectedItem(item); + if (p) { + pieces << p; + } + } + return pieces; +} + +void Palapeli::GamePlay::configure() +{ + if (Palapeli::ConfigDialog().exec() == QDialog::Accepted) { + if (m_playing) { + qDebug() << "SAVING SETTINGS FOR THIS PUZZLE"; + updateSavedGame(); // Save current puzzle Settings. + } + } +} + +//END action handlers + +void Palapeli::GamePlay::loadPuzzle() +{ + qDebug() << "START loadPuzzle()"; + m_restoredGame = false; + // Disable all collection and playing actions during loading. + m_loadingPuzzle = true; + setPalapeliMode(false); + // Stop autosaving and progress-reporting and start the loading-widget. + m_savegameTimer->stop(); // Just in case it is running ... + emit reportProgress(0, 0); + // Is there a saved game? + static const QString pathTemplate = + QString::fromLatin1("collection/%1.save"); + KConfig savedConfig(KStandardDirs::locateLocal("appdata", + pathTemplate.arg(m_puzzle->identifier()))); + if (savedConfig.hasGroup(AppearanceSaveGroup)) { + // Get settings for background, shadows, etc. in this puzzle. + restorePuzzleSettings(&savedConfig); + } + // Return to the event queue to start the loading-widget graphics ASAP. + QTimer::singleShot(0, this, SLOT(loadPuzzleFile())); + qDebug() << "END loadPuzzle()"; +} + +void Palapeli::GamePlay::loadPuzzleFile() +{ + // Clear all scenes, and delete any piece holders that exist. + qDebug() << "Start clearing all scenes: elapsed" << t.elapsed(); + deletePuzzleViews(); + m_viewList << m_puzzleTable->view(); // Re-list the puzzle-table. + qDebug() << "Finish clearing all scenes: elapsed" << t.elapsed(); + + qDebug() << "Start loadPuzzleFile(): elapsed" << t.restart(); + // Begin loading the puzzle. + // It is loaded asynchronously and processed one piece at a time. + m_loadedPieces.clear(); + if (m_puzzle) { + Palapeli::FutureWatcher* watcher = new Palapeli::FutureWatcher; + connect(watcher, SIGNAL(finished()), SLOT(loadNextPiece())); + connect(watcher, SIGNAL(finished()), + watcher, SLOT(deleteLater())); + watcher->setFuture( + m_puzzle->get(Palapeli::PuzzleComponent::Contents)); + } + qDebug() << "Finish loadPuzzleFile(): time" << t.restart(); +} + +void Palapeli::GamePlay::loadNextPiece() +{ + if (!m_puzzle) + return; + const Palapeli::ContentsComponent* component = + m_puzzle->component(); + if (!component) + return; + // Add pieces, but only one at a time. + // PuzzleContents structure is defined in src/file-io/puzzlestructs.h. + // We iterate over contents.pieces: key = pieceID, value = QImage. + const Palapeli::PuzzleContents contents = component->contents; + QMap::const_iterator iterPieces = contents.pieces.begin(); + const QMap::const_iterator iterPiecesEnd = + contents.pieces.end(); + for (int pieceID = iterPieces.key(); iterPieces != iterPiecesEnd; + pieceID = (++iterPieces).key()) + { + if (m_loadedPieces.contains(pieceID)) + continue; // Already loaded. + + // Create a Palapeli::Piece from its image, offsets and ID. + // This also adds bevels, if required. + Palapeli::Piece* piece = new Palapeli::Piece( + iterPieces.value(), contents.pieceOffsets[pieceID]); + piece->addRepresentedAtomicPieces(QList() << pieceID); + piece->addAtomicSize(iterPieces.value().size()); + // IDW test. qDebug() << "PIECE" << pieceID + // << "offset" << contents.pieceOffsets[pieceID] + // << "size" << iterPieces.value().size(); + m_loadedPieces[pieceID] = piece; + piece->completeVisuals(); // Add a shadow, if required. + + // Continue with next piece or next stage, after event loop run. + if (contents.pieces.size() > m_loadedPieces.size()) + QTimer::singleShot(0, this, SLOT(loadNextPiece())); + else + QTimer::singleShot(0, this, SLOT(loadPiecePositions())); + return; + } +} + +void Palapeli::GamePlay::loadPiecePositions() +{ + qDebug() << "Finish loadNextPiece() calls: time" << t.restart(); + if (!m_puzzle) + return; + qDebug() << "loadPiecePositions():"; + m_originalPieceCount = m_loadedPieces.count(); + const Palapeli::PuzzleContents contents = m_puzzle->component()->contents; + //add piece relations + foreach (const DoubleIntPair& relation, contents.relations) { + Palapeli::Piece* firstPiece = + m_loadedPieces.value(relation.first, 0); + Palapeli::Piece* secondPiece = + m_loadedPieces.value(relation.second, 0); + firstPiece->addLogicalNeighbors(QList() + << secondPiece); + secondPiece->addLogicalNeighbors(QList() + << firstPiece); + } + calculatePieceAreaSize(); + m_puzzleTableScene->setPieceAreaSize(m_pieceAreaSize); + + // Is there a saved game? + static const QString pathTemplate = + QString::fromLatin1("collection/%1.save"); + KConfig savedConfig(KStandardDirs::locateLocal("appdata", + pathTemplate.arg(m_puzzle->identifier()))); + bool oldFormat = false; + m_restoredGame = false; + int nHolders = 0; + if (savedConfig.hasGroup(HeaderSaveGroup)) { + KConfigGroup headerGroup(&savedConfig, HeaderSaveGroup); + nHolders = headerGroup.readEntry("N_Holders", 0); + m_restoredGame = true; + } + else if (savedConfig.hasGroup(FormerSaveGroup)) { + m_restoredGame = true; + oldFormat = true; + } + if (m_restoredGame) + { + // IDW TODO - Enable piece-holder actions. + + // Read piece positions from the LocationSaveGroup. + // The current positions of atomic pieces are listed. If + // neighbouring pieces are joined, their position values are + // identical and searchConnections(m_pieces) handles that by + // calling on a MergeGroup object to join the pieces. + + qDebug() << "RESTORING SAVED PUZZLE."; + KConfigGroup holderGroup (&savedConfig, HolderSaveGroup); + KConfigGroup locationGroup (&savedConfig, oldFormat ? + FormerSaveGroup : LocationSaveGroup); + + // Re-create the saved piece-holders, if any. + m_currentHolder = 0; + for (int groupID = 1; groupID <= nHolders; groupID++) { + KConfigGroup holder (&savedConfig, + QString("Holder_%1").arg(groupID)); + // Re-create a piece-holder and add it to m_viewList. + qDebug() << "RE-CREATE HOLDER" + << QString("Holder_%1").arg(groupID) << "name" + << holder.readEntry("Name", QString("")); + createHolder(holder.readEntry("Name", QString("")), + holder.readEntry("Selected", false)); + // Restore the piece-holder's size and position. + QRect r = holder.readEntry("Geometry", QRect()); + qDebug() << "GEOMETRY" << r; + Palapeli::View* v = m_viewList.at(groupID); + v->resize(r.size()); + int x = (r.left() < 0) ? 0 : r.left(); + int y = (r.top() < 0) ? 0 : r.top(); + v->move(x, y); + } + + // Move pieces to saved positions, in holders or puzzle table. + qDebug() << "START POSITIONING PIECES"; + qDebug() << "Old format" << oldFormat << HolderSaveGroup << (oldFormat ? FormerSaveGroup : LocationSaveGroup); + QMap::const_iterator i = + m_loadedPieces.constBegin(); + const QMap::const_iterator end = + m_loadedPieces.constEnd(); + for (int pieceID = i.key(); i != end; pieceID = (++i).key()) + { + Palapeli::Piece* piece = i.value(); + const QString ID = QString::number(pieceID); + const int group = oldFormat ? 0 : + holderGroup.readEntry(ID, 0); + const QPointF p = locationGroup.readEntry(ID, QPointF()); + // qDebug() << "Piece ID" << ID << "group" << group << "pos" << p; + Palapeli::View* view = m_viewList.at(group); + // qDebug() << "View" << (view != 0) << "Scene" << (view->scene() != 0); + view->scene()->addPieceToList(piece); + // qDebug() << "PIECE HAS BEEN ADDED TO SCENE's LIST"; + piece->setPos(p); + // qDebug() << "PIECE HAS BEEN POSITIONED"; + // IDW TODO - Selecting/unselecting did not trigger a + // save. Needed to bring back a "dirty" flag. + // IDW TODO - Same for all other saveable actions? + } + qDebug() << "FINISHED POSITIONING PIECES"; + // Each scene re-merges pieces, as required, with no animation. + foreach (Palapeli::View* view, m_viewList) { + view->scene()->mergeLoadedPieces(); + } + } + else + { + // Place pieces at nice positions. + qDebug() << "GENERATING A NEW PUZZLE BY SHUFFLING."; + // Step 1: determine maximum piece size. + QSizeF pieceAreaSize = m_pieceAreaSize; + m_sizeFactor = 1.0 + 0.05 * Settings::pieceSpacing(); + qDebug() << "PIECE SPACING FACTOR" << m_sizeFactor; + pieceAreaSize *= m_sizeFactor; // Allow more space for pieces. + + // Step 2: place pieces in a grid in random order. + QList piecePool(m_loadedPieces.values()); + int nPieces = piecePool.count(); + Palapeli::ConfigDialog::SolutionSpace space = + (nPieces < 20) ? Palapeli::ConfigDialog::None : + (Palapeli::ConfigDialog::SolutionSpace) + Settings::solutionArea(); + + // Find the size of the area required for the solution. + QRectF r; + foreach (Palapeli::Piece* piece, piecePool) { + r |= piece->sceneBareBoundingRect(); + } + int xResv = 0; + int yResv = 0; + if (space != Palapeli::ConfigDialog::None) { + xResv = r.width()/pieceAreaSize.width() + 1.0; + yResv = r.height()/pieceAreaSize.height() + 1.0; + } + + // To get "a" pieces around the solution, both horizontally and + // vertically, we need to solve for "a" in: + // (a+xResv) * (a+yResv) = piecePool.count() + xResv*yResv + // or a^2 + (xResv+yResv)*a - piecePool.count() = 0 + // Let q = qSqrt(((xResv+yResv)^2 + 4.piecePool.count())), then + // a = (-xResv-yResv +- q)/2, the solution of the quadratic. + // + // The positive root is a = (-xResv - yResv + q)/2. If there is + // no solution area, xResv == yResv == 0 and the above equation + // degenerates to "a" = sqrt(number of pieces), as in earlier + // versions of Palapeli. + + qreal q = qSqrt((xResv + yResv)*(xResv + yResv) + 4*nPieces); + int a = qRound((-xResv-yResv+q)/2.0); + int xMax = xResv + a; + + // Set solution space for None or TopLeft: modify as required. + int x1 = 0; + int y1 = 0; + if (space == Palapeli::ConfigDialog::TopRight) { + x1 = a; + } + else if (space == Palapeli::ConfigDialog::Center) { + x1 = a/2; + y1 = a/2; + } + else if (space == Palapeli::ConfigDialog::BottomLeft) { + y1 = a; + // If the rows are uneven, push the partial row right. + if ((nPieces + xResv*yResv) % xMax) { + yResv++; + } + } + else if (space == Palapeli::ConfigDialog::BottomRight) { + x1 = a; + y1 = a; + } + int x2 = x1 + xResv; + int y2 = y1 + yResv; + qDebug() << "Reserve:" << xResv << yResv << "position" << space; + qDebug() << "Pieces" << piecePool.count() << "rect" << r + << "pieceAreaSize" << pieceAreaSize; + qDebug() << "q" << q << "a" << a << "a/2" << a/2; + qDebug() << "xMax" << xMax << "x1 y1" << x1 << y1 + << "x2 y2" << x2 << y2; + + for (int y = 0; !piecePool.isEmpty(); ++y) { + for (int x = 0; x < xMax && !piecePool.isEmpty(); ++x) { + if ((x >= x1) && (x < x2) && + (y >= y1) && (y < y2)) { + continue; // This space reserved. + } + // Select a random piece. + Palapeli::Piece* piece = piecePool.takeAt( + qrand() % piecePool.count()); + // Place it randomly in grid-cell (x, y). + const QPointF p0(0.0, 0.0); + piece->setPlace(p0, x, y, pieceAreaSize, true); + // Add piece to the puzzle table list (only). + m_puzzleTableScene->addPieceToList(piece); + } + } + // Save the generated puzzle. + // + // If the user goes back to the collection, without making any + // moves, and looks at another puzzle, the generated puzzle + // should not be shuffled again when he/she reloads: only when + // he/she hits Restart Puzzle or chooses to resart a previously + // solved puzzle. + updateSavedGame(); + } + foreach (Palapeli::View* view, m_viewList) { + Palapeli::Scene* scene = view->scene(); + QRectF s = scene->piecesBoundingRect(); + qreal handleWidth = qMin(s.width(), s.height())/100.0; + // Add margin for constraint_handles+spacer and setSceneRect(). + scene->addMargin(handleWidth, 0.5*handleWidth); + scene->addPieceItemsToScene(); + } + qDebug() << "Finish loadPiecePositions(): time" << t.restart(); + finishLoading(); +} + +void Palapeli::GamePlay::finishLoading() +{ + // qDebug() << "finishLoading(): Starting"; + m_puzzle->dropComponent(Palapeli::PuzzleComponent::Contents); + // Start each scene and view. + qDebug() << "COUNTING CURRENT PIECES"; + m_currentPieceCount = 0; + foreach (Palapeli::View* view, m_viewList) { + Palapeli::Scene* scene = view->scene(); + m_currentPieceCount = m_currentPieceCount + + scene->pieces().size(); + qDebug() << "Counted" << scene->pieces().size(); + // IDW TODO - Do this better. It's the VIEWS that need to know. + // IDW TODO - DELETE scene->startPuzzle(); + if (view != m_puzzleTable->view()) { + Palapeli::PieceHolder* holder = + qobject_cast(view); + qDebug() << "Holder" << holder->name() << scene->pieces().size(); + holder->initializeZooming(); + } + else { + qDebug() << "Puzzle table" << scene->pieces().size(); + } + } + m_puzzleTable->view()->puzzleStarted(); + // Initialize external progress display. + emit reportProgress(m_originalPieceCount, m_currentPieceCount); + if (!m_restoredGame && (m_originalPieceCount >= LargePuzzle)) { + // New puzzle and a large one: create a default PieceHolder. + createHolder(i18nc("For holding pieces", "Hand")); + KMessageBox::information(m_mainWindow, + i18nc("Hints for solving large puzzles", + "You have just created a large puzzle: Palapeli has " + "several features to help you solve it within the " + "limited space on the desktop. They are described in " + "detail in the Palapeli Handbook (on the Help menu). " + "Here are just a few quick tips.\n\n" + "Before beginning, it may be best not to use bevels or " + "shadowing with large puzzles (see the Settings " + "dialog), because they make loading slower and " + "highlighting harder to see when the pieces in the " + "view are very small.\n\n" + "The first feature is the puzzle Preview (a picture of " + "the completed puzzle) and a toolbar button to turn it " + "on or off. If you hover over it with the mouse, it " + "magnifies parts of the picture, so the window size " + "you choose for the Preview can be quite small.\n\n" + "Next, there are close-up and distant views of the " + "puzzle table, which you can switch quickly by using " + "a mouse button (default Middle-Click). In close-up " + "view, use the empty space in the scroll bars to " + "search through the puzzle pieces a 'page' at a time. " + "You can adjust the two views by zooming in or out " + "and your changes will be remembered.\n\n" + "Then there is a space on the puzzle table reserved " + "for building up the solution.\n\n" + "Last but not least, there are small windows called " + "'holders'. They are for sorting pieces into groups " + "such as edges, sky or white house on left. You can " + "have as many holders as you like and can give " + "them names. You should already have one named " + "'Hand', for carrying pieces from wherever you find " + "them to the solution area.\n\n" + "You use a special mouse click to transfer pieces into " + "or out of a holder (default Shift Left-Click). First " + "make sure the holder you want to use is active: it " + "should have a blue outline. If not, click on it. To " + "transfer pieces into the holder, select them on the " + "puzzle table then do the special click to 'teleport' " + "them into the holder. Or you can just do the special " + "click on one piece at a time.\n\n" + "To transfer pieces out of a holder, make " + "sure no pieces are selected on the puzzle table, go " + "into the holder window and select some pieces, using " + "normal Palapeli mouse operations, then go back to the " + "puzzle table and do the special click on an empty " + "space where you want the pieces to arrive. Transfer " + "no more than a few pieces at a time, to avoid " + "collisions of pieces on the puzzle table.\n\n" + "By the way, holders can do almost all the things the " + "puzzle table and its window can do, including joining " + "pieces to build up a part of the solution."), + i18nc("Caption for hints", "Solving Large Puzzles"), + QLatin1String("largepuzzle-introduction")); + } + // Check if puzzle has been completed. + if (m_currentPieceCount == 1) { + int result = KMessageBox::questionYesNo(m_mainWindow, + i18n("You have finished the puzzle. Do you want to restart it now?")); + if (result == KMessageBox::Yes) { + restartPuzzle(); + return; + } + } + // Connect moves and merges of pieces to autosaving and progress-report. + foreach (Palapeli::View* view, m_viewList) { + connect(view->scene(), SIGNAL(saveMove(int)), + this, SLOT(positionChanged(int))); + if (view != m_puzzleTable->view()) { + connect(view, + SIGNAL(teleport(Piece*,const QPointF&,View*)), + this, + SLOT(teleport(Piece*,const QPointF&,View*))); + connect(view, SIGNAL(newPieceSelectionSeen(View*)), + this, SLOT(handleNewPieceSelection(View*))); + } + } + // Enable playing actions. + m_loadingPuzzle = false; + setPalapeliMode(true); + qDebug() << "finishLoading(): time" << t.restart(); +} + +void Palapeli::GamePlay::calculatePieceAreaSize() +{ + m_pieceAreaSize = QSizeF(0.0, 0.0); + foreach (Palapeli::Piece* piece, m_loadedPieces) { + m_pieceAreaSize = m_pieceAreaSize.expandedTo + (piece->sceneBareBoundingRect().size()); + } + qDebug() << "m_pieceAreaSize =" << m_pieceAreaSize; +} + +void Palapeli::GamePlay::playVictoryAnimation() +{ + m_puzzleTableScene->setConstrained(true); + QPropertyAnimation* animation = new QPropertyAnimation + (m_puzzleTableScene, "sceneRect", this); + animation->setStartValue(m_puzzleTableScene->sceneRect()); + animation->setEndValue(m_puzzleTableScene->piecesBoundingRect()); + animation->setDuration(1000); + connect(animation, SIGNAL(finished()), + this, SLOT(playVictoryAnimation2())); + animation->start(QAbstractAnimation::DeleteWhenStopped); +} + +void Palapeli::GamePlay::playVictoryAnimation2() +{ + m_puzzleTableScene->setSceneRect(m_puzzleTableScene->piecesBoundingRect()); + QTimer::singleShot(100, this, SIGNAL(victoryAnimationFinished())); + // Give the View some time to play its part of the victory animation. + QTimer::singleShot(1500, this, SLOT(playVictoryAnimation3())); +} + +void Palapeli::GamePlay::playVictoryAnimation3() +{ + KMessageBox::information(m_mainWindow, i18n("Great! You have finished the puzzle.")); +} + +void Palapeli::GamePlay::positionChanged(int reduction) +{ + if (reduction) { + qDebug() << "Reduction:" << reduction << "from" << m_currentPieceCount; + bool victory = (m_currentPieceCount > 1) && + ((m_currentPieceCount - reduction) <= 1); + m_currentPieceCount = m_currentPieceCount - reduction; + emit reportProgress(m_originalPieceCount, m_currentPieceCount); + if (victory) { + playVictoryAnimation(); + } + } + if (!m_savegameTimer->isActive()) + m_savegameTimer->start(); +} + +void Palapeli::GamePlay::updateSavedGame() +{ + static const QString pathTemplate = + QString::fromLatin1("collection/%1.save"); + KConfig savedConfig(KStandardDirs::locateLocal("appdata", + pathTemplate.arg(m_puzzle->identifier()))); + + savePuzzleSettings(&savedConfig); + + // Save the positions of pieces and attributes of piece-holders. + KConfigGroup headerGroup (&savedConfig, HeaderSaveGroup); + KConfigGroup holderGroup (&savedConfig, HolderSaveGroup); + KConfigGroup locationGroup (&savedConfig, LocationSaveGroup); + + headerGroup.writeEntry("N_Holders", m_viewList.count() - 1); + + int groupID = 0; + foreach (Palapeli::View* view, m_viewList) { + bool isHolder = (view != m_puzzleTable->view()); + if (isHolder) { + KConfigGroup holderDetails(&savedConfig, + QString("Holder_%1").arg(groupID)); + Palapeli::PieceHolder* holder = + qobject_cast(view); + bool selected = (view == m_currentHolder); + holderDetails.writeEntry("Name", holder->name()); + holderDetails.writeEntry("Selected", selected); + holderDetails.writeEntry("Geometry", + QRect(view->frameGeometry().topLeft(), view->size())); + } + const QList pieces = view->scene()->pieces(); + foreach (Palapeli::Piece* piece, pieces) { + const QPointF position = piece->pos(); + foreach (int atomicPieceID, piece->representedAtomicPieces()) { + const QString ID = QString::number(atomicPieceID); + locationGroup.writeEntry(ID, position); + if (isHolder) { + holderGroup.writeEntry(ID, groupID); + } + else { + holderGroup.deleteEntry(ID); + } + } + } + groupID++; + } +} + +void Palapeli::GamePlay::savePuzzleSettings(KConfig* savedConfig) +{ + // Save the Appearance settings of the pieces and puzzle background. + KConfigGroup settingsGroup (savedConfig, AppearanceSaveGroup); + settingsGroup.writeEntry("PieceBevelsEnabled", + Settings::pieceBevelsEnabled()); + settingsGroup.writeEntry("PieceShadowsEnabled", + Settings::pieceShadowsEnabled()); + settingsGroup.writeEntry("PieceSpacing", Settings::pieceSpacing()); + settingsGroup.writeEntry("ViewBackground", Settings::viewBackground()); + settingsGroup.writeEntry("ViewBackgroundColor", + Settings::viewBackgroundColor()); + settingsGroup.writeEntry("ViewHighlightColor", + Settings::viewHighlightColor()); + Palapeli::ConfigDialog::SolutionSpace solutionArea = + (Palapeli::ConfigDialog::SolutionSpace) + Settings::solutionArea(); + settingsGroup.writeEntry("SolutionArea", (int)solutionArea); + + // Save the Preview settings. + KConfigGroup previewGroup (savedConfig, PreviewSaveGroup); + previewGroup.writeEntry("PuzzlePreviewGeometry", + Settings::puzzlePreviewGeometry()); + previewGroup.writeEntry("PuzzlePreviewVisible", + Settings::puzzlePreviewVisible()); +} + +void Palapeli::GamePlay::restorePuzzleSettings(KConfig* savedConfig) +{ + // Assume Palapeli::loadPuzzle() has tested if Appearance group exists. + KConfigGroup settingsGroup(savedConfig, AppearanceSaveGroup); + Settings::setPieceBevelsEnabled(settingsGroup.readEntry( + "PieceBevelsEnabled", false)); + Settings::setPieceShadowsEnabled(settingsGroup.readEntry( + "PieceShadowsEnabled", false)); + Settings::setPieceSpacing(settingsGroup.readEntry( + "PieceSpacing", 6)); + Settings::setViewBackground(settingsGroup.readEntry( + "ViewBackground", "background.svg")); + Settings::setViewBackgroundColor(settingsGroup.readEntry( + "ViewBackgroundColor", QColor(0xfff7eb))); + Settings::setViewHighlightColor(settingsGroup.readEntry( + "ViewHighlightColor", QColor(0x6effff))); + Settings::setSolutionArea(settingsGroup.readEntry( + "SolutionArea", 2)); + + // Ask TextureHelper to re-draw background (but only after KConfigDialog + // has written the settings, which might happen after this slot call). + QTimer::singleShot(0, Palapeli::TextureHelper::instance(), + SLOT(readSettings())); + + if (savedConfig->hasGroup(PreviewSaveGroup)) { + KConfigGroup previewGroup(savedConfig, PreviewSaveGroup); + Settings::setPuzzlePreviewGeometry(previewGroup.readEntry( + "PuzzlePreviewGeometry", QRect(-1,-1,320,240))); + Settings::setPuzzlePreviewVisible(previewGroup.readEntry( + "PuzzlePreviewVisible", true)); + } +} + +void Palapeli::GamePlay::changeSelectedHolder(Palapeli::PieceHolder* h) +{ + if (m_currentHolder && (m_currentHolder != h)) { + m_previousHolder = m_currentHolder; + m_currentHolder->setSelected(false); + } + m_currentHolder = h; +} + +#include "gameplay.moc" diff -Nru palapeli-4.12.3/src/engine/gameplay.h palapeli-4.12.90/src/engine/gameplay.h --- palapeli-4.12.3/src/engine/gameplay.h 1970-01-01 00:00:00.000000000 +0000 +++ palapeli-4.12.90/src/engine/gameplay.h 2014-03-10 18:48:34.000000000 +0000 @@ -0,0 +1,145 @@ +/*************************************************************************** + * Copyright 2009 Stefan Majewsky + * Copyright 2014 Ian Wadham + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +***************************************************************************/ + +#ifndef PALAPELI_GAMEPLAY_H +#define PALAPELI_GAMEPLAY_H + +class KConfig; + +class QStackedWidget; + +#include "../window/mainwindow.h" + +#include // IDW test. + +namespace Palapeli +{ + class CollectionView; + class Puzzle; + class PuzzleTableWidget; + class PieceHolder; + class PuzzlePreview; + class View; + class Scene; + class Piece; + + /** + * This is the main class for Palapeli gameplay. It implements menu and + * toolbar actions and provides methods such as loading and shuffling + * a puzzle, starting a puzzle, saving and restoring the state of the + * solution, managing piece-holders, reporting progress and showing + * a victory animation. + */ + + class GamePlay : public QObject + { + Q_OBJECT + public: + GamePlay(MainWindow* mainWindow = 0); + virtual ~GamePlay(); + void init(); + void shutdown(); + CollectionView* collectionView() + { return m_collectionView; }; + PuzzleTableWidget* puzzleTable() + { return m_puzzleTable; }; + static const int LargePuzzle; + public Q_SLOTS: + void playPuzzle(Palapeli::Puzzle* puzzle); + void playPuzzleFile(const QString& path); + void actionGoCollection(); + void actionTogglePreview(); + void actionCreate(); + void actionDelete(); + void actionImport(); + void actionExport(); + void createHolder(); + void deleteHolder(); + void selectAll(); + void rearrangePieces(); + void actionZoomIn(); + void actionZoomOut(); + void restartPuzzle(); + void configure(); + + void positionChanged(int reduction); + Q_SIGNALS: + void reportProgress(int pieceCount, int originalCount); + void victoryAnimationFinished(); + private Q_SLOTS: + void loadPreview(); + void loadPuzzleFile(); + void loadNextPiece(); + void loadPiecePositions(); + void finishLoading(); + + void playVictoryAnimation2(); + void playVictoryAnimation3(); + + void updateSavedGame(); + + void changeSelectedHolder(PieceHolder* h); + void teleport(Piece* piece, const QPointF& scenePos, + View* view); + void closeHolder(PieceHolder* h); + void handleNewPieceSelection(View* view); + + private: + void deletePuzzleViews(); + void loadPuzzle(); + void playVictoryAnimation(); + void calculatePieceAreaSize(); + void createHolder(const QString& name, bool sel = true); + void transferPieces(const QList pieces, + View* source, View* dest, + const QPointF& scenePos = QPointF()); + void setPalapeliMode(bool playing); + QList getSelectedPieces(View* v); + + void savePuzzleSettings(KConfig* savedConfig); + void restorePuzzleSettings(KConfig* savedConfig); + + QStackedWidget* m_centralWidget; + CollectionView* m_collectionView; + PuzzleTableWidget* m_puzzleTable; + PuzzlePreview* m_puzzlePreview; + MainWindow* m_mainWindow; + Puzzle* m_puzzle; + Scene* m_puzzleTableScene; + QList m_viewList; + QSizeF m_pieceAreaSize; + QTimer* m_savegameTimer; + PieceHolder* m_currentHolder; + PieceHolder* m_previousHolder; + + // Some stuff needed for loading puzzles. + bool m_loadingPuzzle; + bool m_restoredGame; + QMap m_loadedPieces; + int m_originalPieceCount; + int m_currentPieceCount; + qreal m_sizeFactor; + bool m_playing; + bool m_canDeletePuzzle; + bool m_canExportPuzzle; + QTime t; // IDW test. + }; +} + +#endif // PALAPELI_GAMEPLAY_H diff -Nru palapeli-4.12.3/src/engine/interactors.cpp palapeli-4.12.90/src/engine/interactors.cpp --- palapeli-4.12.3/src/engine/interactors.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/interactors.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -21,6 +21,9 @@ #include "scene.h" #include "view.h" +#include // IDW test. + +#include #include #include #include @@ -67,6 +70,8 @@ selectedItem->setSelected(false); clickedItem->setSelected(true); m_currentPieces << clickedPiece; + Palapeli::View* v = qobject_cast(this->view()); + v->handleNewPieceSelection(); } } @@ -82,6 +87,7 @@ return false; //start moving this piece determineSelectedItems(selectableItemUnderMouse, piece); + m_baseViewPosition = event.pos; m_baseScenePosition = event.scenePos; m_currentOffset = QPointF(); m_basePositions.clear(); @@ -96,6 +102,11 @@ void Palapeli::MovePieceInteractor::continueInteraction(const Palapeli::MouseEvent& event) { + // Ignore tiny drags. They are probably meant to be a select. + if ((event.pos - m_baseViewPosition).manhattanLength() < + QApplication::startDragDistance()) { + return; + } m_currentOffset = event.scenePos - m_baseScenePosition; for (int i = 0; i < m_currentPieces.count(); ++i) { @@ -148,15 +159,52 @@ if (!selectableItemUnderMouse) return false; //we will only move pieces -> find the piece which we are moving - Palapeli::Piece* currentPiece = Palapeli::Piece::fromSelectedItem(selectableItemUnderMouse); - if (!currentPiece) + m_currentPiece = Palapeli::Piece::fromSelectedItem(selectableItemUnderMouse); + if (!m_currentPiece) return false; //toggle selection state for piece under mouse - currentPiece->setSelected(!currentPiece->isSelected()); + bool previouslySelected = m_currentPiece->isSelected(); + m_currentPiece->setSelected(! previouslySelected); + m_currentPiece->startClick(); + if (! previouslySelected) { + Palapeli::View* v = qobject_cast(this->view()); + v->handleNewPieceSelection(); + } return true; } +void Palapeli::SelectPieceInteractor::stopInteraction(const Palapeli::MouseEvent& event) +{ + Q_UNUSED(event) + m_currentPiece->endClick(); +} + //END Palapeli::SelectPieceInteractor +//BEGIN Palapeli::TeleportPieceInteractor + +Palapeli::TeleportPieceInteractor::TeleportPieceInteractor(QGraphicsView* view) + : Palapeli::Interactor(25, Palapeli::MouseInteractor, view) +{ + setMetadata(PieceInteraction, i18nc("Works instantly, without dragging", "Teleport pieces to or from a holder"), QIcon()); + qDebug() << "CONSTRUCTED TeleportPieceInteractor"; +} + +bool Palapeli::TeleportPieceInteractor::startInteraction(const Palapeli::MouseEvent& event) +{ + qDebug() << "ENTERED TeleportPieceInteractor::startInteraction"; + Palapeli::View* view = qobject_cast(this->view()); + if (!view) + return false; + QGraphicsItem* item = findSelectableItemAt(event.scenePos, scene()); + Palapeli::Piece* piece = 0; + if (item) { + piece = Palapeli::Piece::fromSelectedItem(item); + } + view->teleportPieces(piece, event.scenePos); + return true; +} + +//END Palapeli::TeleportPieceInteractor //BEGIN Palapeli::MoveViewportInteractor Palapeli::MoveViewportInteractor::MoveViewportInteractor(QGraphicsView* view) @@ -180,6 +228,28 @@ } //END Palapeli::MoveViewportInteractor +//BEGIN Palapeli::ToggleCloseUpInteractor + +Palapeli::ToggleCloseUpInteractor::ToggleCloseUpInteractor(QGraphicsView* view) + : Palapeli::Interactor(2, Palapeli::MouseInteractor, view) +{ + // IDW TODO - Check the priority against other priorities. + // + // IDW TODO - What about Palapeli::MouseEvent flags? + setMetadata(ViewportInteraction, i18nc("As in a movie scene", "Switch to close-up or distant view"), KIcon(QLatin1String("zoom-in"))); + qDebug() << "CONSTRUCTED ToggleCloseUpInteractor"; +} + +bool Palapeli::ToggleCloseUpInteractor::startInteraction(const Palapeli::MouseEvent& event) +{ + qDebug() << "ENTERED ToggleCloseUpInteractor::startInteraction"; + Palapeli::View* view = qobject_cast(this->view()); + if (view) + view->toggleCloseUp(); + return true; +} + +//END Palapeli::ToggleCloseUpInteractor //BEGIN Palapeli::ZoomViewportInteractor Palapeli::ZoomViewportInteractor::ZoomViewportInteractor(QGraphicsView* view) @@ -329,7 +399,18 @@ { Q_UNUSED(event) m_item->hide(); //NOTE: This is not necessary for the painting, but we use m_item->isVisible() to determine whether we are rubberbanding at the moment. - m_item->setRect(QRectF()); + m_item->setRect(QRectF()); // Finalise the selection(s), if any. + const QList selectedItems = scene()->selectedItems(); + foreach (QGraphicsItem* selectedItem, selectedItems) { + Palapeli::Piece* selectedPiece = + Palapeli::Piece::fromSelectedItem(selectedItem); + if (selectedPiece) { + Palapeli::View* v = + qobject_cast(this->view()); + v->handleNewPieceSelection(); + break; + } + } } //END Palapeli::RubberBandInteractor diff -Nru palapeli-4.12.3/src/engine/interactors.h palapeli-4.12.90/src/engine/interactors.h --- palapeli-4.12.3/src/engine/interactors.h 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/interactors.h 2014-03-10 18:48:34.000000000 +0000 @@ -47,6 +47,7 @@ QList m_currentPieces; QPointF m_baseScenePosition, m_currentOffset; + QPoint m_baseViewPosition; QList m_basePositions; }; @@ -58,6 +59,31 @@ SelectPieceInteractor(QGraphicsView* view); protected: virtual bool startInteraction(const Palapeli::MouseEvent& event); + virtual void stopInteraction(const Palapeli::MouseEvent& event); + private: + Palapeli::Piece* m_currentPiece; + }; + + //This interactor is assigned to LeftButton;ShiftModifier by default. + // 1. When you click on a piece in the puzzle table it is transferred + // immediately to the currently selected piece-holder. + // 2. When you select one or more pieces on the puzzle table and click + // anywhere on the puzzle table, all the selected pieces are + // transferred immediately to the currently selected piece-holder. + // 3. When you select one or more pieces in the currently selected + // piece-holder and then click anywhere on the puzzle table or in + // another piece-holder, all the selected pieces are transferred + // immediately to the place where you clicked. + // + //The transferred pieces are set selected when they arrive and are + //arranged tidily in a grid pattern. In case 3, the top-left of the + //grid is where you clicked. + class TeleportPieceInteractor : public Palapeli::Interactor + { + public: + TeleportPieceInteractor(QGraphicsView* view); + protected: + virtual bool startInteraction(const Palapeli::MouseEvent& event); }; //This interactor is assigned to RightButton;NoModifier by default. @@ -73,6 +99,16 @@ QPoint m_lastPos; }; + //This interactor is assigned to MidButton;NoModifier by default. + //Clicking on a view will toggle it between close-up and distant views. + class ToggleCloseUpInteractor : public Palapeli::Interactor + { + public: + ToggleCloseUpInteractor(QGraphicsView* view); + protected: + virtual bool startInteraction(const Palapeli::MouseEvent& event); + }; + //This interactor is assigned to wheel:Vertical;NoModifier by default. //Turning the wheel will zoom the viewport. class ZoomViewportInteractor : public Palapeli::Interactor diff -Nru palapeli-4.12.3/src/engine/mergegroup.cpp palapeli-4.12.90/src/engine/mergegroup.cpp --- palapeli-4.12.3/src/engine/mergegroup.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/mergegroup.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -68,11 +68,12 @@ return resultList; } -Palapeli::MergeGroup::MergeGroup(const QList& pieces, QGraphicsScene* scene, bool animated) +Palapeli::MergeGroup::MergeGroup(const QList& pieces, QGraphicsScene* scene, const QSizeF& pieceAreaSize, bool animated) : m_animated(animated) , m_pieces(pieces) , m_mergedPiece(0) , m_scene(scene) + , m_pieceAreaSize(pieceAreaSize) { //find united coordinate system (UCS) -> large pieces contribute more than smaller pieces int totalWeight = 0; @@ -118,6 +119,7 @@ //collect pixmaps for merging (also shadows if possible) QList pieceVisuals; QList shadowVisuals; + QList highlightVisuals; bool allPiecesHaveShadows = true; foreach (Palapeli::Piece* piece, m_pieces) { @@ -130,15 +132,27 @@ else shadowVisuals << shadowSample; } + // Single pieces are assigned highlight items lazily (i.e. if + // they happen to get selected), but when they are merged, each + // one must have a highlight pixmap that can be merged into a + // combined highlight pixmap for the new multi-part piece. + if (!piece->hasHighlight()) { + piece->createHighlight(m_pieceAreaSize); + } + highlightVisuals << piece->highlightVisuals(); } //merge pixmap and create piece Palapeli::PieceVisuals combinedPieceVisuals = Palapeli::mergeVisuals(pieceVisuals); - Palapeli::PieceVisuals combinedShadowVisuals, combinedBeveledVisuals; + Palapeli::PieceVisuals combinedShadowVisuals, combinedHighlightVisuals; if (allPiecesHaveShadows) combinedShadowVisuals = Palapeli::mergeVisuals(shadowVisuals); - m_mergedPiece = new Palapeli::Piece(combinedPieceVisuals, combinedShadowVisuals); + combinedHighlightVisuals = Palapeli::mergeVisuals(highlightVisuals); + m_mergedPiece = new Palapeli::Piece(combinedPieceVisuals, + combinedShadowVisuals, combinedHighlightVisuals); //apply UCS - m_scene->addItem(m_mergedPiece); + if (m_animated) { // If loading the scene, we add the piece later. + m_scene->addItem(m_mergedPiece); + } m_mergedPiece->setPos(m_ucsPosition); //transfer information from old pieces to new piece, then destroy old pieces foreach (Palapeli::Piece* piece, m_pieces) @@ -156,6 +170,9 @@ logicalNeighbor->rewriteLogicalNeighbors(m_pieces, m_mergedPiece); //these neighbors are now represented by m_mergedPiece //transaction done emit pieceInstanceTransaction(m_pieces, QList() << m_mergedPiece); + + // Do not highlight the merged piece, esp. not the solution-in-progress. + m_mergedPiece->setSelected(false); deleteLater(); } diff -Nru palapeli-4.12.3/src/engine/mergegroup.h palapeli-4.12.90/src/engine/mergegroup.h --- palapeli-4.12.3/src/engine/mergegroup.h 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/mergegroup.h 2014-03-10 18:48:34.000000000 +0000 @@ -22,6 +22,7 @@ class QGraphicsScene; #include #include +#include namespace Palapeli { @@ -36,7 +37,7 @@ ///If \a animated is set, the transaction will wait for the animation to finish and then fire the pieceInstanceTransaction() signal. After this emission, the MergeGroup will delete itself. ///If \a animated is not set, you have to obtain the generated piece manually from the mergedPiece() method. - MergeGroup(const QList& pieces, QGraphicsScene* scene, bool animated = true); + MergeGroup(const QList& pieces, QGraphicsScene* scene, const QSizeF& pieceAreaSize, bool animated = true); void start(); @@ -53,6 +54,7 @@ QGraphicsScene* m_scene; //parameters of united coordinate system (UCS) QPointF m_ucsPosition; + QSizeF m_pieceAreaSize; }; } diff -Nru palapeli-4.12.3/src/engine/piece.cpp palapeli-4.12.90/src/engine/piece.cpp --- palapeli-4.12.3/src/engine/piece.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/piece.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -26,6 +26,7 @@ #include #include #include +#include // IDW test. void Palapeli::Piece::commonInit(const Palapeli::PieceVisuals& pieceVisuals) { @@ -40,13 +41,16 @@ // replacing m_pieceItems pixmap (in rerenderBevel()) causes weird pixel errors // when using fast transformation. SmoothTransformation looks better anyway... m_pieceItem->setTransformationMode(Qt::SmoothTransformation); + m_offset = m_pieceItem->offset().toPoint(); } Palapeli::Piece::Piece(const QImage& pieceImage, const QPoint& offset) : m_pieceItem(0) , m_inactiveShadowItem(0) , m_activeShadowItem(0) + , m_highlightItem(0) , m_animator(0) + , m_offset(offset) { //create bevel map if wanted if (Settings::pieceBevelsEnabled()) @@ -63,15 +67,24 @@ commonInit(Palapeli::PieceVisuals(pieceImage, offset)); } -Palapeli::Piece::Piece(const Palapeli::PieceVisuals& pieceVisuals, const Palapeli::PieceVisuals& shadowVisuals) +Palapeli::Piece::Piece(const Palapeli::PieceVisuals& pieceVisuals, const Palapeli::PieceVisuals& shadowVisuals, const Palapeli::PieceVisuals& highlightVisuals) : m_pieceItem(0) , m_inactiveShadowItem(0) , m_activeShadowItem(0) + , m_highlightItem(0) , m_animator(0) + , m_offset(QPoint(0, 0)) // Gets set in Piece::commonInit(). { commonInit(pieceVisuals); if (!shadowVisuals.isNull()) createShadowItems(shadowVisuals); + if (!highlightVisuals.isNull()) { + m_highlightItem = new QGraphicsPixmapItem + (highlightVisuals.pixmap(), this); + m_highlightItem->setOffset(highlightVisuals.offset()); + m_highlightItem->setZValue(-1); + m_highlightItem->setVisible(isSelected()); + } } //BEGIN visuals @@ -91,15 +104,32 @@ return (bool) m_inactiveShadowItem; } +bool Palapeli::Piece::hasHighlight() const +{ + return (bool) m_highlightItem; +} + void Palapeli::Piece::createShadowItems(const Palapeli::PieceVisuals& shadowVisuals) { - const QColor activeShadowColor = QApplication::palette().color(QPalette::Highlight); - const Palapeli::PieceVisuals activeShadowVisuals = Palapeli::changeShadowColor(shadowVisuals, activeShadowColor); - //create inactive shadow item +/* IDW TODO - DELETE this. +#ifdef Q_OS_MAC + // On Apple OS X the QPalette::Highlight color is blue, but is + // dimmed down, for highlighting black-on-white text presumably. + const QColor activeShadowColor(Qt::cyan); + // Note: Q_WS_MAC is deprecated and does not exist in Qt 5. +#else + const QColor activeShadowColor = + QApplication::palette().color(QPalette::Highlight); +#endif +*/ + const QColor activeShadowColor = Settings::viewHighlightColor(); + const Palapeli::PieceVisuals activeShadowVisuals = + Palapeli::changeShadowColor(shadowVisuals, activeShadowColor); + // Create inactive (unhighlighted) shadow item. m_inactiveShadowItem = new QGraphicsPixmapItem(shadowVisuals.pixmap(), this); m_inactiveShadowItem->setOffset(shadowVisuals.offset()); m_inactiveShadowItem->setZValue(-2); - //create active shadow item and animator for its opacity + // Create active shadow item (highlighted) and animator for its opacity. m_activeShadowItem = new QGraphicsPixmapItem(activeShadowVisuals.pixmap(), this); m_activeShadowItem->setOffset(activeShadowVisuals.offset()); m_activeShadowItem->setZValue(-1); @@ -107,6 +137,33 @@ m_animator = new QPropertyAnimation(this, "activeShadowOpacity", this); } +void Palapeli::Piece::createHighlight(const QSizeF& pieceAreaSize) +{ + QRectF rect = sceneBareBoundingRect(); + // IDW TODO - Make the factor an adjustable setting (1.2-2.0). + QSizeF area = 1.5 * pieceAreaSize; + int w = area.width(); + int h = area.height(); + // IDW TODO - Paint pixmap just once (in Scene?) and shallow-copy it. + QRadialGradient g(QPoint(w/2, h/2), qMin(w/2, h/2)); + g.setColorAt(0, Settings::viewHighlightColor()); + g.setColorAt(1,Qt::transparent); + + QPixmap p(w, h); + p.fill(Qt::transparent); + QPainter pa; + pa.begin(&p); + pa.setPen(Qt::NoPen); + pa.setBrush(QBrush(g)); + pa.drawEllipse(0, 0, w, h); + pa.end(); + + m_highlightItem = new QGraphicsPixmapItem(p, this); + m_highlightItem->setOffset(m_offset.x() - w/2 + rect.width()/2, + m_offset.y() - h/2 + rect.height()/2); + m_highlightItem->setZValue(-1); +} + QRectF Palapeli::Piece::bareBoundingRect() const { return m_pieceItem->boundingRect(); @@ -129,6 +186,13 @@ return Palapeli::PieceVisuals(m_inactiveShadowItem->pixmap(), m_inactiveShadowItem->offset().toPoint()); } +Palapeli::PieceVisuals Palapeli::Piece::highlightVisuals() const +{ + if (!m_highlightItem) + return Palapeli::PieceVisuals(); + return Palapeli::PieceVisuals(m_highlightItem->pixmap(), m_highlightItem->offset().toPoint()); +} + qreal Palapeli::Piece::activeShadowOpacity() const { return m_activeShadowItem ? m_activeShadowItem->opacity() : 0.0; @@ -142,7 +206,19 @@ void Palapeli::Piece::pieceItemSelectedChanged(bool selected) { + if (!m_activeShadowItem) { + // No shadows: use a highlighter. + if (!m_highlightItem) { + createHighlight((qobject_cast + (scene()))->pieceAreaSize()); + } + // IDW TODO - Use an animator to change the visibility? + m_highlightItem->setVisible(selected); + return; + } //change visibility of active shadow + // IDW TODO - On select, hide black shadow and brighten highlight. + // m_inactiveShadowItem->setVisible(! selected); // IDW test. const qreal targetOpacity = selected ? 1.0 : 0.0; const qreal opacityDiff = qAbs(targetOpacity - activeShadowOpacity()); if (m_animator && opacityDiff != 0) @@ -172,6 +248,9 @@ void Palapeli::Piece::addLogicalNeighbors(const QList& logicalNeighbors) { foreach (Palapeli::Piece* piece, logicalNeighbors) + // IDW TODO - if (!m_logicalNeighbors.contains(piece) && piece) + // If piece == 0, pieceID was not in m_loadedPieces. + // This would be an integrity error in .puzzle file. if (!m_logicalNeighbors.contains(piece)) m_logicalNeighbors << piece; } @@ -214,6 +293,30 @@ } //END internal datastructures + +void Palapeli::Piece::setPlace(const QPointF& topLeft, int x, int y, + const QSizeF& area, bool random) +{ + const QRectF b = sceneBareBoundingRect(); + const QSizeF pieceSize = b.size(); + QPointF areaOffset; + QPoint pieceOffset = m_pieceItem->offset().toPoint(); + if (random) { + int dx = area.width() - pieceSize.width(); + int dy = area.height() - pieceSize.height(); + areaOffset = QPointF( // Place the piece randomly in the cell. + (dx > 0) ? (qrand() % dx) : 0, // Avoid division by 0. + (dy > 0) ? (qrand() % dy) : 0); + } + else { + areaOffset = QPointF( // Center the piece in the cell. + (area.width() - pieceSize.width())/2.0, + (area.height() - pieceSize.height())/2.0); + } + const QPointF gridPos(x * area.width(), y * area.height()); + setPos(topLeft + gridPos + areaOffset - m_offset); // Move it. +} + //BEGIN mouse interaction bool Palapeli::Piece::isSelected() const @@ -226,6 +329,16 @@ m_pieceItem->setSelected(selected); } +void Palapeli::Piece::startClick() +{ + m_pieceItem->setCursor(Qt::ClosedHandCursor); // Button pressed. +} + +void Palapeli::Piece::endClick() +{ + m_pieceItem->setCursor(Qt::OpenHandCursor); // Button released. +} + Palapeli::Piece* Palapeli::Piece::fromSelectedItem(QGraphicsItem* item) { //We expect: item == piece->m_pieceItem && item->parentItem() == piece @@ -242,17 +355,16 @@ void Palapeli::Piece::doMove() { Palapeli::Scene* scene = qobject_cast(this->scene()); - if (scene) - { + if (scene) { scene->validatePiecePosition(this); - scene->invalidateSavegame(); + emit moved(false); // Still moving. } } void Palapeli::Piece::endMove() { m_pieceItem->setCursor(Qt::OpenHandCursor); - emit moved(); + emit moved(true); // Finishd moving. } //END mouse interaction diff -Nru palapeli-4.12.3/src/engine/piece.h palapeli-4.12.90/src/engine/piece.h --- palapeli-4.12.3/src/engine/piece.h 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/piece.h 2014-03-10 18:48:34.000000000 +0000 @@ -35,10 +35,16 @@ Q_OBJECT Q_PROPERTY(qreal activeShadowOpacity READ activeShadowOpacity WRITE setActiveShadowOpacity) public: - ///This constructor is used when the piece is loaded only from the puzzle file. + /// This constructor is used when the piece is loaded + /// from the puzzle file as a single unjoined piece. explicit Piece(const QImage& pieceImage, const QPoint& offset); - ///This constructor creates a piece without a shadow, unless a shadow is provided explicitly. - explicit Piece(const Palapeli::PieceVisuals& pieceVisuals, const Palapeli::PieceVisuals& shadowVisuals = Palapeli::PieceVisuals()); + /// This constructor is used when several pieces are + /// joined to create a new piece, either after a user's + /// move or when such a piece is re-created at puzzle + /// load time (without animation). The joined piece will + /// be without a shadow, unless shadows are provided + /// explicitly. + explicit Piece(const Palapeli::PieceVisuals& pieceVisuals, const Palapeli::PieceVisuals& shadowVisuals = Palapeli::PieceVisuals(), const Palapeli::PieceVisuals& highlightVisuals = Palapeli::PieceVisuals()); ///This method will ///\li create a shadow for this piece if there is none ATM. ///\li apply the bevel map to the piece pixmap. @@ -51,10 +57,15 @@ QRectF sceneBareBoundingRect() const; Palapeli::PieceVisuals pieceVisuals() const; bool hasShadow() const; + bool hasHighlight() const; Palapeli::PieceVisuals shadowVisuals() const; + Palapeli::PieceVisuals highlightVisuals() const; + void createHighlight(const QSizeF& pieceAreaSize); bool isSelected() const; void setSelected(bool selected); + void startClick(); + void endClick(); ///Returns the corresponding piece for an \a item found e.g. in QGraphicsScene::selectedItems(). This is different from a simple qgraphicsitem_cast because, internally, when you call setSelected(true) on a piece, a child item of this Piece is selected. ///\return 0 if the given \a item does not belong to a Piece static Palapeli::Piece* fromSelectedItem(QGraphicsItem* item); @@ -72,8 +83,11 @@ void rewriteLogicalNeighbors(const QList& oldPieces, Palapeli::Piece* newPiece); ///Call this when this piece instance has been replaced by another piece instance. This will also delete this instance. void announceReplaced(Palapeli::Piece* replacement); + /// Place piece in a grid-cell, randomly or centered. + void setPlace(const QPointF& topLeft, int x, int y, + const QSizeF& area, bool random); Q_SIGNALS: - void moved(); + void moved(bool finished); void replacedBy(Palapeli::Piece* newPiece); protected: friend class MovePieceInteractor; @@ -91,7 +105,9 @@ QGraphicsPixmapItem* m_pieceItem; QGraphicsPixmapItem* m_inactiveShadowItem; QGraphicsPixmapItem* m_activeShadowItem; + QGraphicsPixmapItem* m_highlightItem; // IDW test. QPropertyAnimation* m_animator; + QPoint m_offset; // IDW test. QList m_representedAtomicPieces; QList m_logicalNeighbors; diff -Nru palapeli-4.12.3/src/engine/piecevisuals.cpp palapeli-4.12.90/src/engine/piecevisuals.cpp --- palapeli-4.12.3/src/engine/piecevisuals.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/piecevisuals.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -101,6 +101,9 @@ px.end(); blur(shadowImage, QRect(QPoint(), shadowImage.size()), radius / 3); + // IDW TODO - Shadow and highlight are hard to see on Apple. Can they + // be made more conspicuous? How are they on Linux? + // IDW test. NO divisor ==> "tablets", / 2); ==> a bit bigger than / 3); return shadowImage; } diff -Nru palapeli-4.12.3/src/engine/puzzlepreview.cpp palapeli-4.12.90/src/engine/puzzlepreview.cpp --- palapeli-4.12.3/src/engine/puzzlepreview.cpp 1970-01-01 00:00:00.000000000 +0000 +++ palapeli-4.12.90/src/engine/puzzlepreview.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -0,0 +1,172 @@ +/*************************************************************************** + * Copyright 2010 Johannes Loehnert + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +***************************************************************************/ + +#include "puzzlepreview.h" + +#include "../file-io/puzzlestructs.h" +#include "settings.h" + +#include +#include +#include +#include + +Palapeli::PuzzlePreview::PuzzlePreview(QWidget* parent) +{ + m_settingsSaveTimer = new QTimer(this); + connect(m_settingsSaveTimer, SIGNAL(timeout()), this, SLOT(writeConfigIfGeometryChanged())); + m_geometryChanged = false; + + m_hoverZoom = 1.0; + m_isZoomed = false; + m_mousePos = QPoint(); + + setScene(new QGraphicsScene()); + setParent(parent); + setWindowTitle(i18nc("Window title", "Preview of completed puzzle")); + setWindowFlags(Qt::Tool | Qt::WindowTitleHint); + setAttribute (Qt::WA_NoMousePropagation); // Accept all mouse events. + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setRenderHint(QPainter::SmoothPixmapTransform); + scene()->addText(i18nc("text in preview window", + "Image is not available.")); // Seen if image-data not found. + setSceneRect(scene()->itemsBoundingRect()); + + // read size and position settings + QRect geometry = Settings::puzzlePreviewGeometry(); + resize(geometry.size()); + + // default (-1/-1) toprect: don't change position + if (geometry.left() >= 0 && geometry.top() >= 0) + move(geometry.topLeft()); + + m_settingsSaveTimer->start(500); + hide(); + updateViewport(); +} + +void Palapeli::PuzzlePreview::setImage(const QImage &image) +{ + scene()->clear(); + scene()->addPixmap(QPixmap::fromImage(image)); + setSceneRect(image.rect()); + updateViewport(); +} + +void Palapeli::PuzzlePreview::loadImageFrom(const Palapeli::PuzzleMetadata& md) +{ + // Metadata is assumed to have been loaded by the caller. + setImage(md.image); + setWindowTitle(i18n("%1 - Preview", md.name)); + // Set hover-zoom so that 3x3 pieces would be visible on a square grid. + m_hoverZoom = sqrt(md.pieceCount)/3.0; + if (m_hoverZoom < 1) + m_hoverZoom = 1; +} + +void Palapeli::PuzzlePreview::toggleVisible() +{ + setVisible(!isVisible()); + Settings::setPuzzlePreviewVisible(isVisible()); + Settings::self()->writeConfig(); +} + +void Palapeli::PuzzlePreview::mouseMoveEvent(QMouseEvent* event) +{ + m_mousePos = event->pos(); + updateViewport(); + QGraphicsView::mouseMoveEvent(event); +} + +void Palapeli::PuzzlePreview::enterEvent(QEvent* event) +{ + setMouseTracking(true); + m_isZoomed = true; + m_mousePos = QPoint(); + // wait with update for first mouseMoveEvent + QGraphicsView::enterEvent(event); +} + +void Palapeli::PuzzlePreview::leaveEvent(QEvent* event) +{ + setMouseTracking(false); + m_isZoomed = false; + updateViewport(); + QGraphicsView::leaveEvent(event); +} + +void Palapeli::PuzzlePreview::resizeEvent(QResizeEvent* event) +{ + updateViewport(); + m_geometryChanged = true; + QGraphicsView::resizeEvent(event); +} + +void Palapeli::PuzzlePreview::moveEvent(QMoveEvent* event) +{ + m_geometryChanged = true; + QGraphicsView::moveEvent(event); +} + +void Palapeli::PuzzlePreview::closeEvent(QCloseEvent* event) +{ + // Triggered by the preview window's Close button. + event->accept(); + emit closing(); +} + +void Palapeli::PuzzlePreview::writeConfigIfGeometryChanged() +{ + if (!m_geometryChanged) return; + + // move() includes window frame, resize() doesn't :-/ + Settings::setPuzzlePreviewGeometry(QRect(frameGeometry().topLeft(), size())); + Settings::self()->writeConfig(); + m_geometryChanged = false; +} + +void Palapeli::PuzzlePreview::updateViewport() +{ + qreal zoom; + // calculate zoom for fit-in-window + zoom = width() / sceneRect().width(); + if (zoom > height() / sceneRect().height()) + zoom = height() / sceneRect().height(); + + if (m_isZoomed) + zoom *= m_hoverZoom; + + // do not enlarge + if (zoom>1) + zoom = 1; + + resetTransform(); + scale(zoom, zoom); + + if (m_isZoomed) + { + // focus moves with cursor position + QPointF pos = m_mousePos; + pos.rx() *= sceneRect().width() / width(); + pos.ry() *= sceneRect().height() / height(); + centerOn(pos); + } +} + +#include "puzzlepreview.moc" diff -Nru palapeli-4.12.3/src/engine/puzzlepreview.h palapeli-4.12.90/src/engine/puzzlepreview.h --- palapeli-4.12.3/src/engine/puzzlepreview.h 1970-01-01 00:00:00.000000000 +0000 +++ palapeli-4.12.90/src/engine/puzzlepreview.h 2014-03-10 18:48:34.000000000 +0000 @@ -0,0 +1,68 @@ +/*************************************************************************** + * Copyright 2010 Johannes Loehnert + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +***************************************************************************/ + +#ifndef PALAPELI_PUZZLEPREVIEW_H +#define PALAPELI_PUZZLEPREVIEW_H + +#include +#include + +namespace Palapeli +{ + class PuzzleMetadata; + + class PuzzlePreview : public QGraphicsView + { + Q_OBJECT + public: + PuzzlePreview(QWidget* parent); + + void setImage(const QImage &image); + void loadImageFrom(const Palapeli::PuzzleMetadata& md); + + public Q_SLOTS: + // toggles visibility state AND updates config with the new state. + void toggleVisible(); + + Q_SIGNALS: + void closing(); + + protected: + virtual void mouseMoveEvent(QMouseEvent* event); + virtual void enterEvent(QEvent* event); + virtual void leaveEvent(QEvent* event); + virtual void resizeEvent(QResizeEvent* event); + virtual void moveEvent(QMoveEvent *event); + virtual void closeEvent(QCloseEvent* event); + void updateViewport(); + + private Q_SLOTS: + void writeConfigIfGeometryChanged(); + + private: + // used to save geometry after move/resize, to avoid writing config file each time the cursor moves a pixel. + QTimer* m_settingsSaveTimer; + bool m_geometryChanged; + + qreal m_hoverZoom; + bool m_isZoomed; + QPoint m_mousePos; + }; +} + +#endif // PALAPELI_PUZZLEPREVIEW_H diff -Nru palapeli-4.12.3/src/engine/scene.cpp palapeli-4.12.90/src/engine/scene.cpp --- palapeli-4.12.3/src/engine/scene.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/scene.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -21,40 +21,71 @@ #include "mergegroup.h" #include "piece.h" #include "settings.h" -#include "../file-io/components.h" -#include "../file-io/puzzle.h" -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include - -typedef QPair DoubleIntPair; //comma in type is not possible in foreach macro +#include Palapeli::Scene::Scene(QObject* parent) : QGraphicsScene(parent) , m_constrained(false) , m_constraintVisualizer(new Palapeli::ConstraintVisualizer(this)) , m_puzzle(0) - , m_savegameTimer(new QTimer(this)) - , m_loadingPuzzle(false) + , m_pieceAreaSize(QSizeF(32.0, 32.0)) // Allow 1024 pixels initially. + , m_margin(10.0) + , m_handleWidth(7.0) +{ + initializeGrid(QPointF(0.0, 0.0)); +} + +void Palapeli::Scene::addPieceToList(Palapeli::Piece* piece) +{ + m_pieces << piece; +} + +void Palapeli::Scene::addPieceItemsToScene() { - m_savegameTimer->setInterval(500); //write savegame twice per second at most - m_savegameTimer->setSingleShot(true); - connect(m_savegameTimer, SIGNAL(timeout()), this, SLOT(updateSavegame())); + foreach (Palapeli::Piece * piece, m_pieces) { + addItem(piece); + connect(piece, SIGNAL(moved(bool)), this, SLOT(pieceMoved(bool))); + } } -QRectF Palapeli::Scene::piecesBoundingRect() const +void Palapeli::Scene::dispatchPieces(const QList pieces) { - QRectF result; + foreach (Palapeli::Piece * piece, pieces) { + piece->setSelected(false); + removeItem(piece); + m_pieces.removeAll(piece); + disconnect(piece, SIGNAL(moved(bool)), this, SLOT(pieceMoved(bool))); + } +} + +void Palapeli::Scene::clearPieces() +{ + qDebug() << "Palapeli::Scene Delete" << m_pieces.count() << "pieces in m_pieces list."; + qDeleteAll(m_pieces); + qDebug() << "Palapeli::Scene Clear m_pieces list."; + m_pieces.clear(); + qDebug() << "Palapeli::Scene Stop m_constraintVisualizer."; + m_constraintVisualizer->stop(); +} + +void Palapeli::Scene::addMargin(const qreal handleWidth, const qreal spacer) { + m_handleWidth = handleWidth; + m_margin = handleWidth + spacer; + QRectF r = piecesBoundingRect(); + r.adjust(-m_margin, -m_margin, m_margin, m_margin); + setSceneRect(r); + m_constraintVisualizer->start(r, handleWidth); + views()[0]->fitInView(r, Qt::KeepAspectRatio); + qDebug() << "SCENE RECT" << r << "VIEW SIZE" << views()[0]->size(); +} + +QRectF Palapeli::Scene::piecesBoundingRect(const int minGrid) const +{ + // Space is >= minGrid*minGrid pieces (e.g. for a new PieceHolder). + QSizeF minSize = minGrid * m_gridSpacing; + QRectF result (QPointF(0.0, 0.0), minSize); foreach (Palapeli::Piece* piece, m_pieces) result |= piece->sceneBareBoundingRect(); return result; @@ -76,16 +107,18 @@ void Palapeli::Scene::validatePiecePosition(Palapeli::Piece* piece) { - //get system geometry + // Get current scene rectangle. const QRectF sr = sceneRect(); - const QRectF br = piece->sceneBareBoundingRect(); //br = bounding rect + // Get bounding rectangle of all pieces and add margin. + QRectF br = piece->sceneBareBoundingRect(); //br = bounding rect + br.adjust(-m_margin, -m_margin, m_margin, m_margin); if (sr.contains(br)) return; - //check constraint - if (m_constrained) - { + + // Check for constraint (ie. pieces must not "push" puzzle table edges). + if (m_constrained) { + // Constraint active -> make sure piece stays inside scene rect. QPointF pos = piece->pos(); - //scene rect constraint is active -> ensure that piece stays inside scene rect if (br.left() < sr.left()) pos.rx() += sr.left() - br.left(); if (br.right() > sr.right()) @@ -96,25 +129,41 @@ pos.ry() += sr.bottom() - br.bottom(); piece->setPos(pos); } - else - //scene rect constraint is not active -> enlarge scene rect as necessary + else { + // Constraint not active -> enlarge scene rect as necessary. setSceneRect(sr | br); + } } -void Palapeli::Scene::searchConnections(const QList& pieces) +void Palapeli::Scene::mergeLoadedPieces() { + // After loading, merge previously assembled pieces, with no animation. + // We need to check all the loaded atomic pieces in each scene. + searchConnections(m_pieces, false); +} + +void Palapeli::Scene::searchConnections(const QList& pieces, + const bool animatedMerging) +{ + // Look for pieces that can be joined after moving or loading. + // If any are found, merge them, with or without animation. QList uncheckedPieces(pieces); - const bool animatedMerging = !m_loadingPuzzle; while (!uncheckedPieces.isEmpty()) { Palapeli::Piece* piece = uncheckedPieces.takeFirst(); - const QList pieceGroup = Palapeli::MergeGroup::tryGrowMergeGroup(piece); + const QList pieceGroup = + Palapeli::MergeGroup::tryGrowMergeGroup(piece); foreach (Palapeli::Piece* checkedPiece, pieceGroup) uncheckedPieces.removeAll(checkedPiece); if (pieceGroup.size() > 1) { - Palapeli::MergeGroup* mergeGroup = new Palapeli::MergeGroup(pieceGroup, this, animatedMerging); - connect(mergeGroup, SIGNAL(pieceInstanceTransaction(QList,QList)), this, SLOT(pieceInstanceTransaction(QList,QList))); + Palapeli::MergeGroup* mergeGroup = + new Palapeli::MergeGroup(pieceGroup, this, + m_pieceAreaSize, animatedMerging); + connect(mergeGroup, SIGNAL(pieceInstanceTransaction( + QList,QList)), + this, SLOT(pieceInstanceTransaction( + QList,QList))); mergeGroup->start(); } } @@ -122,188 +171,27 @@ void Palapeli::Scene::pieceInstanceTransaction(const QList& deletedPieces, const QList& createdPieces) { + // qDebug() << "Scene::pieceInstanceTransaction(delete" << deletedPieces.count() << "add" << createdPieces.count(); const int oldPieceCount = m_pieces.count(); foreach (Palapeli::Piece* oldPiece, deletedPieces) m_pieces.removeAll(oldPiece); //these pieces have been deleted by the caller foreach (Palapeli::Piece* newPiece, createdPieces) { m_pieces << newPiece; - connect(newPiece, SIGNAL(moved()), this, SLOT(pieceMoved())); - } - if (!m_loadingPuzzle) - { - emit reportProgress(m_atomicPieceCount, m_pieces.count()); - //victory animation - if (m_pieces.count() == 1 && oldPieceCount > 1) - QTimer::singleShot(0, this, SLOT(playVictoryAnimation())); - } -} - -void Palapeli::Scene::loadPuzzle(Palapeli::Puzzle* puzzle) -{ - if (m_loadingPuzzle) - return; - //load puzzle - if (puzzle && m_puzzle != puzzle) - { - m_puzzle = puzzle; - loadPuzzleInternal(); - } -} - -void Palapeli::Scene::loadPuzzleInternal() -{ - m_loadingPuzzle = true; - //reset behavioral parameters - setConstrained(false); - //clear scene - qDeleteAll(m_pieces); m_pieces.clear(); - emit reportProgress(0, 0); - //begin to load puzzle - m_loadedPieces.clear(); - if (m_puzzle) - { - Palapeli::FutureWatcher* watcher = new Palapeli::FutureWatcher; - connect(watcher, SIGNAL(finished()), SLOT(loadNextPiece())); - connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater())); - watcher->setFuture(m_puzzle->get(Palapeli::PuzzleComponent::Contents)); - } -} - -void Palapeli::Scene::loadNextPiece() -{ - if (!m_puzzle) - return; - const Palapeli::ContentsComponent* component = m_puzzle->component(); - if (!component) - return; - //add pieces, but only one at a time - const Palapeli::PuzzleContents contents = component->contents; - QMap::const_iterator iterPieces = contents.pieces.begin(); - const QMap::const_iterator iterPiecesEnd = contents.pieces.end(); - for (int pieceID = iterPieces.key(); iterPieces != iterPiecesEnd; pieceID = (++iterPieces).key()) - { - if (m_loadedPieces.contains(pieceID)) - continue; //already loaded - //load piece - Palapeli::Piece* piece = new Palapeli::Piece(iterPieces.value(), contents.pieceOffsets[pieceID]); - piece->addRepresentedAtomicPieces(QList() << pieceID); - piece->addAtomicSize(iterPieces.value().size()); - addItem(piece); - m_pieces << piece; - m_loadedPieces[pieceID] = piece; - connect(piece, SIGNAL(moved()), this, SLOT(pieceMoved())); - //continue with next piece after eventloop run - if (contents.pieces.size() > m_pieces.size()) - QTimer::singleShot(0, this, SLOT(loadNextPiece())); - else - QTimer::singleShot(0, this, SLOT(loadPiecePositions())); - return; + connect(newPiece, SIGNAL(moved(bool)), + this, SLOT(pieceMoved(bool))); } + // qDebug() << "emit saveMove(" << oldPieceCount - m_pieces.count(); + emit saveMove(oldPieceCount - m_pieces.count()); } -void Palapeli::Scene::loadPiecePositions() +void Palapeli::Scene::pieceMoved(bool finished) { - if (!m_puzzle) + if (!finished) { + emit saveMove(0); return; - const Palapeli::PuzzleContents contents = m_puzzle->component()->contents; - //add piece relations - foreach (const DoubleIntPair& relation, contents.relations) - { - Palapeli::Piece* firstPiece = m_pieces[relation.first]; - Palapeli::Piece* secondPiece = m_pieces[relation.second]; - firstPiece->addLogicalNeighbors(QList() << secondPiece); - secondPiece->addLogicalNeighbors(QList() << firstPiece); - } - //Is "savegame" available? - static const QString pathTemplate = QString::fromLatin1("collection/%1.save"); - KConfig saveConfig(KStandardDirs::locateLocal("appdata", pathTemplate.arg(m_puzzle->identifier()))); - if (saveConfig.hasGroup("SaveGame")) - { - //read piece positions from savegame - KConfigGroup saveGroup(&saveConfig, "SaveGame"); - QMap::const_iterator iterPieces = m_loadedPieces.constBegin(); - const QMap::const_iterator iterPiecesEnd = m_loadedPieces.constEnd(); - for (int pieceID = iterPieces.key(); iterPieces != iterPiecesEnd; pieceID = (++iterPieces).key()) - { - Palapeli::Piece* piece = iterPieces.value(); - piece->setPos(saveGroup.readEntry(QString::number(pieceID), QPointF())); - } - searchConnections(m_pieces); - } - else - { - //place pieces at nice positions - //step 1: determine maximum piece size - QSizeF pieceAreaSize; - foreach (Palapeli::Piece* piece, m_pieces) - pieceAreaSize = pieceAreaSize.expandedTo(piece->sceneBareBoundingRect().size()); - pieceAreaSize *= 1.3; //more space for each piece - //step 2: place pieces in a grid in random order - QList piecePool(m_pieces); - const int xCount = floor(qSqrt(piecePool.count())); - for (int y = 0; !piecePool.isEmpty(); ++y) - { - for (int x = 0; x < xCount && !piecePool.isEmpty(); ++x) - { - //select random piece - Palapeli::Piece* piece = piecePool.takeAt(qrand() % piecePool.count()); - //determine piece offset - piece->setPos(QPointF()); - const QRectF br = piece->sceneBareBoundingRect(); - const QPointF pieceOffset = br.topLeft(); - const QSizeF pieceSize = br.size(); - //determine random position inside piece area - const QPointF areaOffset( - qrand() % (int)(pieceAreaSize.width() - pieceSize.width()), - qrand() % (int)(pieceAreaSize.height() - pieceSize.height()) - ); - //move to desired position in (x,y) grid - const QPointF gridBasePosition(x * pieceAreaSize.width(), y * pieceAreaSize.height()); - piece->setPos(gridBasePosition + areaOffset - pieceOffset); - } - } - } - //continue after eventloop run - QTimer::singleShot(0, this, SLOT(completeVisualsForNextPiece())); -} - -void Palapeli::Scene::completeVisualsForNextPiece() -{ - foreach (Palapeli::Piece* piece, m_pieces) - { - if (piece->completeVisuals()) - { - //something had to be done -> continue with next piece after eventloop run - QTimer::singleShot(0, this, SLOT(completeVisualsForNextPiece())); - return; - } - } - //no pieces without shadow left, or piece visuals completely disabled - finishLoading(); -} - -void Palapeli::Scene::finishLoading() -{ - m_puzzle->dropComponent(Palapeli::PuzzleComponent::Contents); - //determine scene rect - setSceneRect(piecesBoundingRect()); - //initialize external progress display - m_atomicPieceCount = m_loadedPieces.count(); - emit reportProgress(m_atomicPieceCount, m_pieces.count()); - emit puzzleStarted(); - m_loadingPuzzle = false; - //check if puzzle has been completed - if (m_pieces.count() == 1) - { - int result = KMessageBox::questionYesNo(views()[0], i18n("You have finished the puzzle the last time. Do you want to restart it now?")); - if (result == KMessageBox::Yes) - restartPuzzle(); } -} - -void Palapeli::Scene::pieceMoved() -{ + int before = m_pieces.count(); QList mergeCandidates; foreach (QGraphicsItem* item, selectedItems()) { @@ -311,63 +199,41 @@ if (piece) mergeCandidates << piece; } - searchConnections(mergeCandidates); - invalidateSavegame(); - emit reportProgress(m_atomicPieceCount, m_pieces.count()); -} - -void Palapeli::Scene::invalidateSavegame() -{ - if (!m_savegameTimer->isActive()) - m_savegameTimer->start(); + searchConnections(mergeCandidates, true); // With animation. } -void Palapeli::Scene::updateSavegame() +void Palapeli::Scene::initializeGrid(const QPointF& gridTopLeft) { - //save piece positions - static const QString pathTemplate = QString::fromLatin1("collection/%1.save"); - KConfig saveConfig(KStandardDirs::locateLocal("appdata", pathTemplate.arg(m_puzzle->identifier()))); - KConfigGroup saveGroup(&saveConfig, "SaveGame"); - foreach (Palapeli::Piece* piece, m_pieces) - { - const QPointF pos = piece->pos(); - foreach (int atomicPieceID, piece->representedAtomicPieces()) - saveGroup.writeEntry(QString::number(atomicPieceID), pos); + m_gridTopLeft = gridTopLeft; + m_gridSpacing = pieceAreaSize()*(1.0 + 0.05 * Settings::pieceSpacing()); + m_gridRank = 1; + m_gridX = 0; + m_gridY = 0; + // qDebug() << "GRID INITIALIZED" << m_gridTopLeft + // << "spacing" << m_gridSpacing << "scene size" << sceneRect(); +} + +void Palapeli::Scene::addToGrid(Palapeli::Piece* piece) +{ + // qDebug() << "ADD TO GRID AT" << m_gridTopLeft + // << QPoint(m_gridX, m_gridY) + // << "spacing" << m_gridSpacing << "random" << false; + piece->setPlace(m_gridTopLeft, m_gridX, m_gridY, m_gridSpacing, false); + // Calculate the next spot on the square grid. + if (m_gridY == (m_gridRank - 1)) { + m_gridX++; // Add to bottom row. + if (m_gridX > (m_gridRank - 1)) { + m_gridRank++; // Expand the square grid. + m_gridX = m_gridRank - 1; + m_gridY = 0; // Start right-hand column. + } } -} - -void Palapeli::Scene::playVictoryAnimation() -{ - setConstrained(true); - QPropertyAnimation* animation = new QPropertyAnimation(this, "sceneRect", this); - animation->setStartValue(sceneRect()); - animation->setEndValue(piecesBoundingRect()); - animation->setDuration(1000); - connect(animation, SIGNAL(finished()), this, SLOT(playVictoryAnimation2())); - animation->start(QAbstractAnimation::DeleteWhenStopped); -} - -void Palapeli::Scene::playVictoryAnimation2() -{ - setSceneRect(piecesBoundingRect()); - QTimer::singleShot(100, this, SIGNAL(victoryAnimationFinished())); - QTimer::singleShot(1500, this, SLOT(playVictoryAnimation3())); //give the View some time to play its part of the victory animation -} - -void Palapeli::Scene::playVictoryAnimation3() -{ - KMessageBox::information(views()[0], i18n("Great! You have finished the puzzle.")); -} - -void Palapeli::Scene::restartPuzzle() -{ - if (!m_puzzle) { - return; // If no puzzle was successfully loaded and started. + else { + m_gridY++; // Add to right-hand column. + if (m_gridY == (m_gridRank - 1)) { + m_gridX = 0; // Start bottom row. + } } - static const QString pathTemplate = QString::fromLatin1("collection/%1.save"); - QFile(KStandardDirs::locateLocal("appdata", pathTemplate.arg(m_puzzle->identifier()))).remove(); - //reload puzzle - loadPuzzleInternal(); } #include "scene.moc" diff -Nru palapeli-4.12.3/src/engine/scene.h palapeli-4.12.90/src/engine/scene.h --- palapeli-4.12.3/src/engine/scene.h 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/scene.h 2014-03-10 18:48:34.000000000 +0000 @@ -19,10 +19,7 @@ #ifndef PALAPELI_SCENE_H #define PALAPELI_SCENE_H -#include "basics.h" - #include -#include namespace Palapeli { @@ -30,53 +27,75 @@ class Piece; class Puzzle; + /** + * This class holds the puzzle pieces and boundary (constraint) of a + * Palapeli scene, which can be either a piece-holder or the main + * puzzle table. The scene also handles adding and removing pieces, + * moving pieces, merging (or joining) pieces, arranging pieces into + * a grid and signalling changes in the state of the puzzle and its + * pieces, wherever they may be. + */ + class Scene : public QGraphicsScene { Q_OBJECT public: Scene(QObject* parent = 0); + void addPieceToList(Palapeli::Piece* piece); + void addPieceItemsToScene(); bool isConstrained() const; - QRectF piecesBoundingRect() const; + QRectF piecesBoundingRect(const int minGrid = 2) const; + qreal margin() { return m_margin; } + qreal handleWidth() { return m_handleWidth; } + void addMargin(const qreal handleWidth, + const qreal spacer); void validatePiecePosition(Palapeli::Piece* piece); - void searchConnections(const QList& pieces); + void mergeLoadedPieces(); + const QSizeF& pieceAreaSize() + { return m_pieceAreaSize; } + void setPieceAreaSize(const QSizeF& pieceAreaSize) + { m_pieceAreaSize = pieceAreaSize; } + QList pieces() { return m_pieces; } + + void dispatchPieces(const QList pcs); + void clearPieces(); + + void initializeGrid(const QPointF& gridTopLeft); + void addToGrid(Piece* piece); + public Q_SLOTS: - void loadPuzzle(Palapeli::Puzzle* puzzle); - void restartPuzzle(); void setConstrained(bool constrained); - void invalidateSavegame(); Q_SIGNALS: void constrainedChanged(bool constrained); - void puzzleStarted(); - void reportProgress(int pieceCount, int atomicPieceCount); - void victoryAnimationFinished(); + void saveMove(int reduction); private Q_SLOTS: - void pieceMoved(); - void pieceInstanceTransaction(const QList& deletedPieces, const QList& createdPieces); - void updateSavegame(); - void playVictoryAnimation(); - void playVictoryAnimation2(); - void playVictoryAnimation3(); - //loading steps - void loadNextPiece(); - void loadPiecePositions(); - void completeVisualsForNextPiece(); - void finishLoading(); + void pieceMoved(bool finished); + void pieceInstanceTransaction( + const QList& deletedPieces, + const QList& createdPieces); private: - void loadPuzzleInternal(); - + void searchConnections( + const QList& pieces, + const bool animatedMerging = true); //behavior parameters bool m_constrained; Palapeli::ConstraintVisualizer* m_constraintVisualizer; //game parameters Palapeli::Puzzle* m_puzzle; QList m_pieces; - QTimer* m_savegameTimer; int m_atomicPieceCount; - //some stuff needed for loading puzzles - bool m_loadingPuzzle; - QMap m_loadedPieces; + QSizeF m_pieceAreaSize; + // Width of ConstraintVisualizer and space at edges. + qreal m_margin; + qreal m_handleWidth; + + QPointF m_gridTopLeft; + QSizeF m_gridSpacing; + int m_gridRank; + int m_gridX; + int m_gridY; }; } diff -Nru palapeli-4.12.3/src/engine/triggermapper.cpp palapeli-4.12.90/src/engine/triggermapper.cpp --- palapeli-4.12.3/src/engine/triggermapper.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/triggermapper.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -38,7 +38,9 @@ QMap result; result["MovePiece"] = new Palapeli::MovePieceInteractor(view); result["SelectPiece"] = new Palapeli::SelectPieceInteractor(view); + result["TeleportPiece"] = new Palapeli::TeleportPieceInteractor(view); result["MoveViewport"] = new Palapeli::MoveViewportInteractor(view); + result["ToggleCloseUp"] = new Palapeli::ToggleCloseUpInteractor(view); result["ZoomViewport"] = new Palapeli::ZoomViewportInteractor(view); result["ScrollViewportHoriz"] = new Palapeli::ScrollViewportInteractor(Qt::Horizontal, view); result["ScrollViewportVert"] = new Palapeli::ScrollViewportInteractor(Qt::Vertical, view); @@ -53,7 +55,9 @@ QMap result; result.insert("MovePiece", Palapeli::Trigger("LeftButton;NoModifier")); result.insert("SelectPiece", Palapeli::Trigger("LeftButton;ControlModifier")); + result.insert("TeleportPiece", Palapeli::Trigger("LeftButton;ShiftModifier")); result.insert("MoveViewport", Palapeli::Trigger("RightButton;NoModifier")); + result.insert("ToggleCloseUp", Palapeli::Trigger("MidButton;NoModifier")); result.insert("ZoomViewport", Palapeli::Trigger("wheel:Vertical;NoModifier")); result.insert("RubberBand", Palapeli::Trigger("LeftButton;NoModifier")); result.insert("Constraints", Palapeli::Trigger("LeftButton;NoModifier")); @@ -79,6 +83,7 @@ void Palapeli::TriggerMapper::readSettings() { m_associations.clear(); + m_associations = Palapeli::TriggerMapper::defaultAssociations(); //read config KConfigGroup group(KGlobal::config(), "Mouse Interaction"); const QStringList configKeys = group.keyList(); @@ -87,12 +92,12 @@ const QByteArray interactorKey = configKey.toLatin1(); const QList triggers = group.readEntry(configKey, QList()); foreach (const Palapeli::Trigger& trigger, triggers) //implicit casts FTW - if (trigger.isValid()) + if (trigger.isValid()) { + // Remove default and insert config value(s). + m_associations.remove(interactorKey); m_associations.insertMulti(interactorKey, trigger); + } } - //fallback to default settings if necessary - if (m_associations.isEmpty()) - m_associations = Palapeli::TriggerMapper::defaultAssociations(); //announce update to InteractorManagers emit associationsChanged(); } diff -Nru palapeli-4.12.3/src/engine/view.cpp palapeli-4.12.90/src/engine/view.cpp --- palapeli-4.12.3/src/engine/view.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/view.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -19,6 +19,7 @@ #include "view.h" #include "interactormanager.h" #include "scene.h" +#include "piece.h" #include "texturehelper.h" #include @@ -28,21 +29,40 @@ #include #include +#include +#include + +#include +#include // IDW test. + const int Palapeli::View::MinimumZoomLevel = 0; const int Palapeli::View::MaximumZoomLevel = 200; +const int DefaultDelta = 120; Palapeli::View::View() : m_interactorManager(new Palapeli::InteractorManager(this)) , m_scene(0) - , m_zoomLevel(100) + , m_zoomLevel(MinimumZoomLevel) + , m_closeUpLevel(MaximumZoomLevel) + , m_distantLevel(MinimumZoomLevel) + , m_isCloseUp(false) + , m_dZoom(20.0) + , m_minScale(0.01) + , m_adjustPointer(false) { setFrameStyle(QFrame::NoFrame); setMouseTracking(true); setResizeAnchor(QGraphicsView::AnchorUnderMouse); setTransformationAnchor(QGraphicsView::AnchorUnderMouse); setScene(new Palapeli::Scene(this)); - connect(m_scene, SIGNAL(puzzleStarted()), this, SLOT(puzzleStarted())); - connect(m_scene, SIGNAL(victoryAnimationFinished()), this, SLOT(startVictoryAnimation())); + connect(m_scene, SIGNAL(sceneRectChanged(QRectF)), this, SLOT(logSceneChange(QRectF))); // IDW test. + qDebug() << "Initial size of Palapeli::View" << size(); +} + +// IDW test. +void Palapeli::View::logSceneChange(QRectF r) +{ + qDebug() << "View::logSceneChange" << r << "View size" << this->size(); } Palapeli::InteractorManager* Palapeli::View::interactorManager() const @@ -63,8 +83,10 @@ this->QGraphicsView::setScene(m_scene); m_interactorManager->updateScene(); Palapeli::TextureHelper::instance()->addScene(m_scene); - //reset zoom level (TODO: store viewport geometry in Scene) - zoomTo(100); + // Draw empty, hidden scene: needed to get first load resized correctly. + scene->addMargin(20.0, 10.0); + // Set zoom level to middle of range. + zoomTo((MaximumZoomLevel+MinimumZoomLevel)/2); } QRectF Palapeli::View::viewportRect() const @@ -126,53 +148,193 @@ verticalScrollBar()->setValue(verticalScrollBar()->value() - sceneDelta.y()); } +void Palapeli::View::teleportPieces(Piece* pieceUnder, const QPointF& scenePos) +{ + qDebug() << "TELEPORT: pieceUnder" << (pieceUnder != 0) + << "scenePos" << scenePos; + emit teleport(pieceUnder, scenePos, this); +} + void Palapeli::View::zoomBy(int delta) { + // Scroll wheel and touchpad come here. + // Delta is typically +-120 per click for a mouse-wheel, but can be <10 + // for an Apple MacBook touchpad (using two fingers to scroll). + + // IDW TODO - Accept deltas of <10, either by accumulating deltas or by + // implementing fractional zoom levels. + qDebug() << "View::zoomBy: delta" << delta; + m_adjustPointer = true; zoomTo(m_zoomLevel + delta / 10); } void Palapeli::View::zoomTo(int level) { - //validate/normalize input + // IDW TODO - BUG: If you zoom out as far as Palapeli will go, using the + // scroll-wheel, then go on scrolling, the view will zoom in + // and back out again momentarily. + + // Validate/normalize input. level = qBound(MinimumZoomLevel, level, MaximumZoomLevel); - //skip unimportant requests - if (level == m_zoomLevel) + // Skip unimportant requests. + if (level == m_zoomLevel) { return; - //create a new transform - const QPointF center = mapToScene(rect().center()); - const qreal scalingFactor = pow(2, (level - 100) / 30.0); - QTransform t; - t.translate(center.x(), center.y()); - t.scale(scalingFactor, scalingFactor); + } + // Save the mouse position in both view and scene. + m_mousePos = mapFromGlobal(QCursor::pos()); + m_scenePos = mapToScene(m_mousePos); + // Create a new transform. + const qreal scalingFactor = m_minScale * pow(2, level/m_dZoom); + qDebug() << "View::zoomTo: level" << level + << "scalingFactor" << scalingFactor + << m_mousePos << m_scenePos; + // Translation, shear, etc. are the same: only the scale is replaced. + QTransform t = transform(); + t.setMatrix(scalingFactor, t.m12(), t.m13(), + t.m21(), scalingFactor, t.m23(), + t.m31(), t.m32(), t.m33()); setTransform(t); - //save and report changes + // Save and report changes. m_zoomLevel = level; emit zoomLevelChanged(m_zoomLevel); + // In a mouse-centered zoom, lock the pointer onto the scene position. + if (m_adjustPointer) { + // Let the new view settle down before checking the mouse. + QTimer::singleShot(0, this, SLOT(adjustPointer())); + } +} + +void Palapeli::View::adjustPointer() +{ + // If the view moved, keep the mouse at the same position in the scene. + const QPoint mousePos = mapFromScene(m_scenePos); + if (mousePos != m_mousePos) { + qDebug() << "POINTER MOVED from" << m_mousePos + << "to" << mousePos << "scenePos" << m_scenePos; + QCursor::setPos(mapToGlobal(mousePos)); + } +} + +void Palapeli::View::zoomSliderInput(int level) +{ + if (level == m_zoomLevel) { + return; // Avoid echo from zoomLevelChanged() signal. + } + m_adjustPointer = false; + zoomTo(level); } void Palapeli::View::zoomIn() { - zoomBy(120); + // ZoomWidget ZoomIn button comes here via zoomInRequest signal. + // ZoomIn menu and shortcut come here via GamePlay::actionZoomIn. + m_adjustPointer = false; + zoomTo(m_zoomLevel + DefaultDelta / 10); } void Palapeli::View::zoomOut() { - zoomBy(-120); + // ZoomWidget ZoomOut button comes here via zoomOutRequest signal. + // ZoomOut menu and shortcut come here via GamePlay::actionZoomOut. + m_adjustPointer = false; + zoomTo(m_zoomLevel - DefaultDelta / 10); +} + +// IDW TODO - Keyboard shortcuts for moving the view left, right, up or down by +// one "frame" or "page". Map to Arrow keys, PageUp and PageDown. +// Use QAbstractScrollArea (inherited by QGraphicsView) to get the +// QScrollBar objects (horizontal and vertical). QAbstractSlider, +// an ancestor of QScrollBar, contains position info, signals and +// triggers for scroll bar moves (i.e. triggerAction(action type)). + +// NOTE: We must have m_closeUpLevel >= (m_distantLevel + MinDiff) at all times. +const int MinDiff = 10; // Minimum separation of the two zoom levels. + +void Palapeli::View::toggleCloseUp() +{ + m_isCloseUp = !m_isCloseUp; // Switch to the other view. + m_adjustPointer = true; + if (m_isCloseUp) { + // Save distant level as we leave: in case it changed. + m_distantLevel = (m_zoomLevel <= (m_closeUpLevel - MinDiff)) ? + m_zoomLevel : m_closeUpLevel - MinDiff; + zoomTo(m_closeUpLevel); + } + else { + // Save close-up level as we leave: in case it changed. + m_closeUpLevel = (m_zoomLevel >= (m_distantLevel + MinDiff)) ? + m_zoomLevel : m_distantLevel + MinDiff; + zoomTo(m_distantLevel); + } +} + +void Palapeli::View::handleNewPieceSelection() +{ + emit newPieceSelectionSeen(this); +} + +qreal Palapeli::View::calculateCloseUpScale() +{ + // Get the size of the monitor on which this view resides (in pixels). + const QRect monitor = QApplication::desktop()->screenGeometry(this); + const int pixelsPerPiece = qMin(monitor.width(), monitor.height())/12; + QSizeF size = scene()->pieceAreaSize(); + qreal scale = pixelsPerPiece/qMin(size.rwidth(),size.rheight()); + return scale; +} + +int Palapeli::View::calculateZoomRange(qreal distantScale, bool distantView) +{ + qreal closeUpScale = calculateCloseUpScale(); + if (closeUpScale < distantScale) { + closeUpScale = distantScale; // View is already large enough. + } + qDebug() << "View::calculateZoomRange: distantScale" << distantScale + << "distantView" << distantView + << "closeUpScale" << closeUpScale; + const qreal minScale = distantScale*0.75; + const qreal maxScale = closeUpScale*2.0; + const qreal range = log(maxScale/minScale)/log(2.0); + const qreal dZoom = (MaximumZoomLevel - MinimumZoomLevel)/range; + qDebug() << "minScale" << minScale << "maxScale" << maxScale + << "range" << range << "dZoom" << dZoom; + m_dZoom = dZoom; + m_minScale = minScale; + + // Set the toggling levels. If close-up is too small, adjust it. + m_distantLevel = qRound(dZoom*log(distantScale/minScale)/log(2.0));; + m_closeUpLevel = qRound(MaximumZoomLevel - MinimumZoomLevel - m_dZoom); + m_closeUpLevel = (m_closeUpLevel >= (m_distantLevel + MinDiff)) ? + m_closeUpLevel : m_distantLevel + MinDiff; + m_isCloseUp = (! distantView); // Start with the view zoomed in or out. + const int level = (distantView ? m_distantLevel : m_closeUpLevel); + qDebug() << "INITIAL LEVEL" << level + << "toggles" << m_distantLevel << m_closeUpLevel; + return level; } void Palapeli::View::puzzleStarted() { - resetTransform(); - //scale viewport to show the whole puzzle table - const QRectF sr = sceneRect(); - const QRectF vr = mapToScene(viewport()->rect()).boundingRect(); - const qreal scalingFactor = 0.9 * qMin(vr.width() / sr.width(), vr.height() / sr.height()); //factor 0.9 avoids that scene rect touches viewport bounds (which does not look nice) - const int level = 100 + (int)(30.0 * (log(scalingFactor) / log(2.0))); - zoomTo(level); - centerOn(sr.center()); - emit zoomAdjustable(true); - //explain autosaving + qDebug() << "ENTERED View::puzzleStarted()"; + // At this point the puzzle pieces have been shuffled or loaded from a + // .save file and the puzzle table has been scaled to fit the view. Now + // adjust zooming and slider to a range of distant and close-up views. + + // Choose the lesser of the horizontal and vertical scaling factors. + const qreal distantScale = qMin(transform().m11(), transform().m22()); + qDebug() << "distantScale" << distantScale; + // Calculate the zooming range and return the distant scale's level. + int level = calculateZoomRange(distantScale, true); + + // Don't readjust the zoom. Just set the slider pointer. + m_zoomLevel = level; // Make zoomTo() ignore the back-signal. + emit zoomLevelChanged(level); + centerOn(sceneRect().center()); // Center the view of the whole puzzle. + emit zoomAdjustable(true); // Enable the ZoomWidget. + + // Explain autosaving. KMessageBox::information(window(), i18n("Your progress is saved automatically while you play."), i18nc("used as caption for a dialog that explains the autosave feature", "Automatic saving"), QLatin1String("autosave-introduction")); + qDebug() << "EXITING View::puzzleStarted()"; } void Palapeli::View::startVictoryAnimation() diff -Nru palapeli-4.12.3/src/engine/view.h palapeli-4.12.90/src/engine/view.h --- palapeli-4.12.3/src/engine/view.h 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/view.h 2014-03-10 18:48:34.000000000 +0000 @@ -26,6 +26,7 @@ class ConstraintVisualizer; class InteractorManager; class Scene; + class Piece; class View : public QGraphicsView { @@ -34,18 +35,24 @@ public: View(); + void puzzleStarted(); Palapeli::InteractorManager* interactorManager() const; Palapeli::Scene* scene() const; QRectF viewportRect() const; void setViewportRect(const QRectF& viewportRect); + void teleportPieces(Piece* piece, const QPointF& scPos); + void toggleCloseUp(); + void handleNewPieceSelection(); static const int MinimumZoomLevel; static const int MaximumZoomLevel; public Q_SLOTS: + void logSceneChange(QRectF r); // IDW test. void setScene(Palapeli::Scene* scene); void moveViewportBy(const QPointF& sceneDelta); + void zoomSliderInput(int level); void zoomIn(); void zoomOut(); void zoomBy(int delta); //delta = 0 -> no change, delta < 0 -> zoom out, delta > 0 -> zoom in @@ -60,14 +67,28 @@ Q_SIGNALS: void zoomLevelChanged(int level); void zoomAdjustable(bool adjustable); + void teleport(Piece* p, const QPointF& scPos, View* v); + void newPieceSelectionSeen(View* v); + protected: + virtual int calculateZoomRange(qreal distantScale, + bool distantView); + virtual qreal calculateCloseUpScale(); private Q_SLOTS: - void puzzleStarted(); void startVictoryAnimation(); + void adjustPointer(); private: Palapeli::InteractorManager* m_interactorManager; Palapeli::Scene* m_scene; QPointF m_dragPrevPos; int m_zoomLevel; + int m_closeUpLevel; + int m_distantLevel; + bool m_isCloseUp; + qreal m_dZoom; + qreal m_minScale; + QPoint m_mousePos; + QPointF m_scenePos; + bool m_adjustPointer; }; } diff -Nru palapeli-4.12.3/src/engine/zoomwidget.cpp palapeli-4.12.90/src/engine/zoomwidget.cpp --- palapeli-4.12.3/src/engine/zoomwidget.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/engine/zoomwidget.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -54,7 +54,7 @@ layout->addWidget(m_zoomOutButton); layout->addWidget(m_slider); layout->addWidget(m_zoomInButton); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); } diff -Nru palapeli-4.12.3/src/main.cpp palapeli-4.12.90/src/main.cpp --- palapeli-4.12.3/src/main.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/main.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -28,8 +28,11 @@ int main(int argc, char** argv) { qsrand(time(0)); - KAboutData about("palapeli", 0, ki18nc("The application's name", "Palapeli"), "1.2", ki18n("KDE Jigsaw Puzzle Game"), KAboutData::License_GPL, ki18n("Copyright 2009, 2010, Stefan Majewsky")); + KAboutData about("palapeli", 0, ki18nc("The application's name", "Palapeli"), "2.0", ki18n("KDE Jigsaw Puzzle Game"), KAboutData::License_GPL, ki18n("Copyright 2009, 2010, Stefan Majewsky")); about.addAuthor(ki18n("Stefan Majewsky"), KLocalizedString(), "majewsky@gmx.net", "http://majewsky.wordpress.com"); + about.addCredit (ki18n ("Johannes Loehnert"), + ki18n ("The option to preview the completed puzzle"), + "loehnert.kde@gmx.de"); KCmdLineArgs::init(argc, argv, &about); KCmdLineOptions options; diff -Nru palapeli-4.12.3/src/palapeli.kcfg palapeli-4.12.90/src/palapeli.kcfg --- palapeli-4.12.3/src/palapeli.kcfg 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/palapeli.kcfg 2014-03-10 18:48:34.000000000 +0000 @@ -14,7 +14,19 @@ - white + #fff7eb + + + + #6effff + + + + 0 + + + + 6 @@ -22,11 +34,21 @@ - true + false + false + + + + + true + + + QRect(-1, -1, 320, 240) + diff -Nru palapeli-4.12.3/src/palapeliui.rc palapeli-4.12.90/src/palapeliui.rc --- palapeli-4.12.3/src/palapeliui.rc 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/palapeliui.rc 2014-03-10 18:48:34.000000000 +0000 @@ -1,30 +1,42 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + + Main toolbar - - + + - - + + - + + diff -Nru palapeli-4.12.3/src/settings.ui palapeli-4.12.90/src/settings.ui --- palapeli-4.12.3/src/settings.ui 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/settings.ui 2014-03-10 18:48:34.000000000 +0000 @@ -6,11 +6,11 @@ 0 0 - 461 - 327 + 551 + 519 - + @@ -18,25 +18,47 @@ - - - Background: + + + + + Background: + + + + + + + + 0 + 0 + + + + + + + + + + + 255 + 255 + 255 + - - - - - 0 - 0 - + + + + Color for highlighting selected pieces: - - - + + + 255 255 @@ -45,6 +67,86 @@ + + + + + + Space for solution: + + + + + + + + 0 + 0 + + + + + + + + + + No space is provided with puzzles of less than 20 pieces. Changes will take effect when a puzzle is created or re-started. + + + true + + + + + + + + + Spacing of pieces in puzzle grids (1.0-1.5): + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 0 + + + 10 + + + 1 + + + 6 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 2 + + + + + @@ -53,7 +155,7 @@ Piece appearance - + @@ -92,10 +194,7 @@ Piece behavior - - - QFormLayout::ExpandingFieldsGrow - + @@ -128,7 +227,7 @@ - + @@ -185,7 +284,7 @@ 20 - 40 + 34 @@ -194,16 +293,16 @@ - KComboBox - QComboBox -
kcombobox.h
-
- KColorButton QPushButton
kcolorbutton.h
+ KComboBox + QComboBox +
kcombobox.h
+
+ Palapeli::TriggerComboBox KComboBox
config/configdialog_p.h
diff -Nru palapeli-4.12.3/src/window/mainwindow.cpp palapeli-4.12.90/src/window/mainwindow.cpp --- palapeli-4.12.3/src/window/mainwindow.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/window/mainwindow.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -17,202 +17,144 @@ ***************************************************************************/ #include "mainwindow.h" + +#include "../engine/gameplay.h" + #include "puzzletablewidget.h" -#include "../config/configdialog.h" -#include "../creator/puzzlecreator.h" -#include "../engine/scene.h" -#include "../engine/view.h" -#include "../file-io/collection.h" #include "../file-io/collection-view.h" -#include "../file-io/components.h" -#include "../file-io/puzzle.h" #include "settings.h" -#include #include #include #include -#include #include -#include #include #include -//TODO: move LoadingWidget into here (stack into m_centralWidget) - Palapeli::MainWindow::MainWindow(KCmdLineArgs* args) - : m_centralWidget(new QStackedWidget) - , m_collectionView(new Palapeli::CollectionView) - , m_puzzleTable(new Palapeli::PuzzleTableWidget) + : m_game(new Palapeli::GamePlay(this)) { setupActions(); //setup GUI KXmlGuiWindow::StandardWindowOptions guiOptions = KXmlGuiWindow::Default; guiOptions &= ~KXmlGuiWindow::StatusBar; //no statusbar setupGUI(QSize(500, 500), guiOptions); - //setup collection view - m_collectionView->setModel(Palapeli::Collection::instance()); - connect(m_collectionView, SIGNAL(playRequest(Palapeli::Puzzle*)), SLOT(playPuzzle(Palapeli::Puzzle*))); - //setup puzzle table - m_puzzleTable->showStatusBar(Settings::showStatusBar()); - //setup central widget - m_centralWidget->addWidget(m_collectionView); - m_centralWidget->addWidget(m_puzzleTable); - m_centralWidget->setCurrentWidget(m_collectionView); - setCentralWidget(m_centralWidget); + m_game->init(); //start a puzzle if a puzzle URL has been given if (args->count() != 0) { - const QString path = args->arg(0); - const QString id = Palapeli::Puzzle::fsIdentifier(path); - playPuzzle(new Palapeli::Puzzle(new Palapeli::ArchiveStorageComponent, path, id)); + m_game->playPuzzleFile(args->arg(0)); } args->clear(); } +bool Palapeli::MainWindow::queryClose() +{ + // Terminate cleanly if the user Quits when playing a puzzle. + m_game->shutdown(); + return true; +} + void Palapeli::MainWindow::setupActions() { - //standard stuff - KStandardAction::preferences(this, SLOT(configure()), actionCollection()); - KAction* statusBarAct = KStandardAction::showStatusbar(m_puzzleTable, SLOT(showStatusBar(bool)), actionCollection()); + // Standard stuff. + KStandardAction::preferences(m_game, SLOT(configure()), + actionCollection()); + KAction* statusBarAct = KStandardAction::showStatusbar + (m_game->puzzleTable(), SLOT(showStatusBar(bool)), actionCollection()); statusBarAct->setChecked(Settings::showStatusBar()); statusBarAct->setText(i18n("Show statusbar of puzzle table")); - //Back to collection + + // Back to collection. KAction* goCollAct = new KAction(KIcon("go-previous"), i18n("Back to &collection"), 0); goCollAct->setToolTip(i18n("Go back to the collection to choose another puzzle")); goCollAct->setEnabled(false); //because the collection is initially shown - actionCollection()->addAction("go_collection", goCollAct); - connect(goCollAct, SIGNAL(triggered()), SLOT(actionGoCollection())); - //Create new puzzle (FIXME: action should have a custom icon) + actionCollection()->addAction("view_collection", goCollAct); + connect(goCollAct, SIGNAL(triggered()), m_game, SLOT(actionGoCollection())); + + // Create new puzzle (FIXME: action should have a custom icon). KAction* createAct = new KAction(KIcon("tools-wizard"), i18n("Create &new puzzle..."), 0); createAct->setShortcut(KStandardShortcut::openNew()); createAct->setToolTip(i18n("Create a new puzzle using an image file from your disk")); - actionCollection()->addAction("file_new", createAct); - connect(createAct, SIGNAL(triggered()), SLOT(actionCreate())); - //Delete - KAction* deleteAct = new KAction(KIcon("archive-remove"), i18n("&Delete"), 0); + actionCollection()->addAction("game_new", createAct); + connect(createAct, SIGNAL(triggered()), m_game, SLOT(actionCreate())); + + // Delete a puzzle. + KAction* deleteAct = new KAction(KIcon("archive-remove"), i18n("&Delete puzzle"), 0); deleteAct->setEnabled(false); //will be enabled when something is selected deleteAct->setToolTip(i18n("Delete the selected puzzle from your collection")); - actionCollection()->addAction("file_delete", deleteAct); - connect(m_collectionView, SIGNAL(canDeleteChanged(bool)), deleteAct, SLOT(setEnabled(bool))); - connect(deleteAct, SIGNAL(triggered()), SLOT(actionDelete())); - //Import from file... + actionCollection()->addAction("game_delete", deleteAct); + connect(m_game->collectionView(), SIGNAL(canDeleteChanged(bool)), deleteAct, SLOT(setEnabled(bool))); + connect(deleteAct, SIGNAL(triggered()), m_game, SLOT(actionDelete())); + + // Import from file... KAction* importAct = new KAction(KIcon("document-import"), i18n("&Import from file..."), 0); importAct->setToolTip(i18n("Import a new puzzle from a file into your collection")); - actionCollection()->addAction("file_import", importAct); - connect(importAct, SIGNAL(triggered()), this, SLOT(actionImport())); - //Export to file... + actionCollection()->addAction("game_import", importAct); + connect(importAct, SIGNAL(triggered()), m_game, SLOT(actionImport())); + + // Export to file... KAction* exportAct = new KAction(KIcon("document-export"), i18n("&Export to file..."), 0); exportAct->setEnabled(false); //will be enabled when something is selected exportAct->setToolTip(i18n("Export the selected puzzle from your collection into a file")); - actionCollection()->addAction("file_export", exportAct); - connect(m_collectionView, SIGNAL(canExportChanged(bool)), exportAct, SLOT(setEnabled(bool))); - connect(exportAct, SIGNAL(triggered()), this, SLOT(actionExport())); - //Restart puzzle (TODO: placed here only temporarily) + actionCollection()->addAction("game_export", exportAct); + connect(m_game->collectionView(), SIGNAL(canExportChanged(bool)), exportAct, SLOT(setEnabled(bool))); + connect(exportAct, SIGNAL(triggered()), m_game, SLOT(actionExport())); + + //Reshuffle and restart puzzle KAction* restartPuzzleAct = new KAction(KIcon("view-refresh"), i18n("&Restart puzzle..."), 0); - restartPuzzleAct->setToolTip(i18n("Delete the saved progress")); + restartPuzzleAct->setToolTip(i18n("Delete the saved progress and reshuffle the pieces")); restartPuzzleAct->setEnabled(false); //no puzzle in progress initially actionCollection()->addAction("game_restart", restartPuzzleAct); - connect(restartPuzzleAct, SIGNAL(triggered()), m_puzzleTable->view()->scene(), SLOT(restartPuzzle())); -} - -//BEGIN action handlers - -void Palapeli::MainWindow::configure() -{ - Palapeli::ConfigDialog().exec(); + connect(restartPuzzleAct, SIGNAL(triggered()), m_game, SLOT(restartPuzzle())); + // Quit. + KStandardAction::quit (this, SLOT (close()), actionCollection()); + // Create piece-holder. + KAction* createHolderAct = new KAction(i18n("&Create piece holder..."), 0); + createHolderAct->setToolTip(i18n("Create a temporary holder for sorting pieces")); + createHolderAct->setShortcut(QKeySequence(Qt::Key_C)); + actionCollection()->addAction("move_create_holder", createHolderAct); + connect(createHolderAct, SIGNAL(triggered()), m_game, SLOT(createHolder())); + + // Delete piece-holder. + KAction* deleteHolderAct = new KAction(i18n("&Delete piece holder"), 0); + deleteHolderAct->setToolTip(i18n("Delete a selected temporary holder when it is empty")); + deleteHolderAct->setShortcut(QKeySequence(Qt::Key_D)); + actionCollection()->addAction("move_delete_holder", deleteHolderAct); + connect(deleteHolderAct, SIGNAL(triggered()), m_game, SLOT(deleteHolder())); + + // Select all pieces in a piece-holder. + KAction* selectAllAct = new KAction(i18n("&Select all in holder"), 0); + selectAllAct->setToolTip(i18n("Select all pieces in a selected piece holder")); + selectAllAct->setShortcut(QKeySequence(Qt::Key_A)); + actionCollection()->addAction("move_select_all", selectAllAct); + connect(selectAllAct, SIGNAL(triggered()), m_game, SLOT(selectAll())); + + // Rearrange a selected piece-holder or selected pieces in any view. + KAction* rearrangeAct = new KAction(i18n("&Rearrange pieces"), 0); + rearrangeAct->setToolTip(i18n("Rearrange all pieces in a selected piece holder or selected pieces in any window")); + rearrangeAct->setShortcut(QKeySequence(Qt::Key_R)); + actionCollection()->addAction("move_rearrange", rearrangeAct); + connect(rearrangeAct, SIGNAL(triggered()), m_game, SLOT(rearrangePieces())); + + // Toggle puzzle-preview. + bool isVisible = Settings::puzzlePreviewVisible(); + const QString text = i18nc("Preview is a noun here", "&Preview"); + KToggleAction* togglePreviewAct = new KToggleAction(KIcon("view-preview"), text, 0); + togglePreviewAct->setIconText(i18nc("Preview is a noun here", "Preview")); + togglePreviewAct->setToolTip(i18n("Show or hide the image of the completed puzzle")); + actionCollection()->addAction("view_preview", togglePreviewAct); + togglePreviewAct->setEnabled(false); + togglePreviewAct->setChecked(false); + connect(togglePreviewAct, SIGNAL(triggered()), m_game, SLOT(actionTogglePreview())); + + // View zoom in. + KStandardAction::zoomIn(m_game, SLOT(actionZoomIn()), + actionCollection()); + + // View zoom out. + KStandardAction::zoomOut(m_game, SLOT(actionZoomOut()), + actionCollection()); } -void Palapeli::MainWindow::playPuzzle(Palapeli::Puzzle* puzzle) -{ - if (!puzzle) - return; - m_puzzleTable->view()->scene()->loadPuzzle(puzzle); - m_centralWidget->setCurrentWidget(m_puzzleTable); - actionCollection()->action("go_collection")->setEnabled(true); - actionCollection()->action("game_restart")->setEnabled(true); - //load caption from metadata - puzzle->get(Palapeli::PuzzleComponent::Metadata).waitForFinished(); - const Palapeli::MetadataComponent* cmp = puzzle->component(); - setCaption(cmp ? cmp->metadata.name : QString()); -} - -void Palapeli::MainWindow::actionGoCollection() -{ - m_centralWidget->setCurrentWidget(m_collectionView); - actionCollection()->action("go_collection")->setEnabled(false); - actionCollection()->action("game_restart")->setEnabled(false); - setCaption(QString()); -} - -void Palapeli::MainWindow::actionCreate() -{ - QPointer creatorDialog(new Palapeli::PuzzleCreatorDialog); - if (creatorDialog->exec()) - { - if (!creatorDialog) - return; - Palapeli::Puzzle* puzzle = creatorDialog->result(); - if (!puzzle) { - delete creatorDialog; - return; - } - Palapeli::Collection::instance()->importPuzzle(puzzle); - playPuzzle(puzzle); - } - delete creatorDialog; -} - -void Palapeli::MainWindow::actionDelete() -{ - QModelIndexList indexes = m_collectionView->selectedIndexes(); - //ask user for confirmation - QStringList puzzleNames; - foreach (const QModelIndex& index, indexes) - puzzleNames << index.data(Qt::DisplayRole).toString(); - const int result = KMessageBox::warningContinueCancelList(this, i18n("The following puzzles will be deleted. This action cannot be undone."), puzzleNames); - if (result != KMessageBox::Continue) - return; - //do deletion - Palapeli::Collection* coll = Palapeli::Collection::instance(); - foreach (const QModelIndex& index, indexes) - coll->deletePuzzle(index); -} - -void Palapeli::MainWindow::actionImport() -{ - const QString filter = i18nc("Filter for a file dialog", "*.puzzle|Palapeli puzzles (*.puzzle)"); - const QStringList paths = KFileDialog::getOpenFileNames(KUrl("kfiledialog:///palapeli-import"), filter); - Palapeli::Collection* coll = Palapeli::Collection::instance(); - foreach (const QString& path, paths) - coll->importPuzzle(path); -} - -void Palapeli::MainWindow::actionExport() -{ - QModelIndexList indexes = m_collectionView->selectedIndexes(); - Palapeli::Collection* coll = Palapeli::Collection::instance(); - foreach (const QModelIndex& index, indexes) - { - Palapeli::Puzzle* puzzle = coll->puzzleFromIndex(index); - if (!puzzle) - continue; - //get puzzle name (as an initial guess for the file name) - puzzle->get(Palapeli::PuzzleComponent::Metadata).waitForFinished(); - const Palapeli::MetadataComponent* cmp = puzzle->component(); - if (!cmp) - continue; - //ask user for target file name - const QString startLoc = QString::fromLatin1("kfiledialog:///palapeli-export/%1.puzzle").arg(cmp->metadata.name); - const QString filter = i18nc("Filter for a file dialog", "*.puzzle|Palapeli puzzles (*.puzzle)"); - const QString location = KFileDialog::getSaveFileName(KUrl(startLoc), filter); - if (location.isEmpty()) - continue; //process aborted by user - //do export - coll->exportPuzzle(index, location); - } -} - -//END action handlers - #include "mainwindow.moc" diff -Nru palapeli-4.12.3/src/window/mainwindow.h palapeli-4.12.90/src/window/mainwindow.h --- palapeli-4.12.3/src/window/mainwindow.h 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/window/mainwindow.h 2014-03-10 18:48:34.000000000 +0000 @@ -25,29 +25,23 @@ namespace Palapeli { + class GamePlay; class CollectionView; class Puzzle; class PuzzleTableWidget; + class PuzzlePreview; class MainWindow : public KXmlGuiWindow { Q_OBJECT public: MainWindow(KCmdLineArgs* args); - public Q_SLOTS: - void configure(); - void playPuzzle(Palapeli::Puzzle* puzzle); - void actionGoCollection(); - void actionCreate(); - void actionDelete(); - void actionImport(); - void actionExport(); + protected: + virtual bool queryClose(); private: void setupActions(); - QStackedWidget* m_centralWidget; - Palapeli::CollectionView* m_collectionView; - Palapeli::PuzzleTableWidget* m_puzzleTable; + Palapeli::GamePlay* m_game; }; } diff -Nru palapeli-4.12.3/src/window/pieceholder.cpp palapeli-4.12.90/src/window/pieceholder.cpp --- palapeli-4.12.3/src/window/pieceholder.cpp 1970-01-01 00:00:00.000000000 +0000 +++ palapeli-4.12.90/src/window/pieceholder.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -0,0 +1,100 @@ +/*************************************************************************** + * Copyright 2014 Ian Wadham + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +***************************************************************************/ + +#include "pieceholder.h" +#include "../engine/scene.h" +#include "../engine/piece.h" +#include "settings.h" + +#include +#include + +const qreal minGrid = 2.0; // 2x2 pieces in close-up of minimum holder. +const qreal maxGrid = 6.0; // 6x6 pieces in distant view of min holder. + +Palapeli::PieceHolder::PieceHolder(QWidget* parent, const QSizeF& pieceArea, + const QString& title) + : m_scene(scene()) +{ + qDebug() << "CONSTRUCTING Palapeli::PieceHolder" << title; + setParent(parent); + setWindowFlags(Qt::Window | Qt::Tool | Qt::WindowTitleHint); + // Allow space for (2 * 2) pieces in minimum view initially. + m_scene->setPieceAreaSize(pieceArea); + m_scene->initializeGrid(QPointF(0.0, 0.0)); + m_scene->setSceneRect(m_scene->piecesBoundingRect(minGrid)); + setWindowTitle(title); + qreal s = calculateCloseUpScale(); + QRectF r = m_scene->sceneRect(); + setMinimumSize(s*r.width()+1.0, s*r.height()+1.0); + resize(minimumSize()); + qDebug() << "Close-up scale" << s << "pieceArea" << pieceArea + << "size" << size(); + QTransform t; + t.scale(s, s); + setTransform(t); + centerOn(r.center()); + setSelected(true); + show(); +} + +void Palapeli::PieceHolder::initializeZooming() +{ + // Allow space for a distant view of at most (maxGrid * maxGrid) pieces + // in a piece holder when the view is at minimum size. More that number + // of pieces can be teleported in, but the holder window will have to be + // resized or scrolled for the user to see them, even in distant view. + + qDebug() << "ENTERED PieceHolder::initializeZooming() for" << name(); + qreal scale = qMin(transform().m11(), transform().m22()); + scale = scale * (minGrid / maxGrid); + // Calculate the zooming range and return the close-up scale's level. + int level = calculateZoomRange(scale, false); + zoomTo(level); + centerOn(sceneRect().center()); +} + +void Palapeli::PieceHolder::focusInEvent(QFocusEvent* e) +{ + // The user selected this piece holder. + Q_UNUSED(e) + qDebug() << "PieceHolder::focusInEvent()" << windowTitle(); + setSelected(true); + emit selected(this); // De-select the previously selected holder. +} + +void Palapeli::PieceHolder::setSelected(bool onOff) +{ + qDebug() << "PieceHolder::setSelected()" << windowTitle() << onOff; + setStyleSheet(QString("QFrame { border: 3px solid %1; }").arg + (onOff ? "blue" : "lightGray")); +} + +void Palapeli::PieceHolder::closeEvent(QCloseEvent* event) +{ + // Triggered by the piece-holder window's Close button. + if(m_scene->pieces().isEmpty()) { + event->accept(); // The window can be closed. + } + else { + event->ignore(); // The window cannot be closed. + } + emit closing(this); // GamePlay handles the details. +} + +#include "pieceholder.moc" diff -Nru palapeli-4.12.3/src/window/pieceholder.h palapeli-4.12.90/src/window/pieceholder.h --- palapeli-4.12.3/src/window/pieceholder.h 1970-01-01 00:00:00.000000000 +0000 +++ palapeli-4.12.90/src/window/pieceholder.h 2014-03-10 18:48:34.000000000 +0000 @@ -0,0 +1,64 @@ +/*************************************************************************** + * Copyright 2014 Ian Wadham + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +***************************************************************************/ + +#ifndef PALAPELI_PIECEHOLDER_H +#define PALAPELI_PIECEHOLDER_H + +#include "../engine/view.h" + +class QCloseEvent; + +namespace Palapeli +{ + class Scene; + class Piece; + + /** + * Objects of this class are small windows that hold pieces temporarily + * while a large Palapeli jigsaw puzzle is being solved. There may be + * any number of such windows, including none in small puzzles. The + * pieces in a holder will usually have something in common, as decided + * by the user. For example, they might represent sky, skyline, water or + * other parts of the picture. In any large puzzle, there is a default + * holder called "Hand", which represents a player collecting pieces in + * his or her hand, then moving to the solution area to place them. + * + * The class has methods to assist in collecting and organizing pieces. + */ + + class PieceHolder : public View + { + Q_OBJECT + public: + PieceHolder(QWidget* parent, const QSizeF& pieceArea, + const QString& title); + void initializeZooming(); + void setSelected(bool onOff); + QString name() { return windowTitle(); } + protected: + virtual void focusInEvent(QFocusEvent* e); + virtual void closeEvent(QCloseEvent* event); + Q_SIGNALS: + void selected(PieceHolder* h); + void closing(PieceHolder* h); + private: + Scene* m_scene; + }; +} + +#endif // PALAPELI_PIECEHOLDER_H diff -Nru palapeli-4.12.3/src/window/puzzletablewidget.cpp palapeli-4.12.90/src/window/puzzletablewidget.cpp --- palapeli-4.12.3/src/window/puzzletablewidget.cpp 2014-01-03 07:27:28.000000000 +0000 +++ palapeli-4.12.90/src/window/puzzletablewidget.cpp 2014-03-10 18:48:34.000000000 +0000 @@ -23,6 +23,7 @@ #include "../engine/zoomwidget.h" #include "settings.h" +#include #include #include #include @@ -56,9 +57,10 @@ { //setup progress bar m_progressBar->setText(i18n("No puzzle loaded")); - connect(m_view->scene(), SIGNAL(reportProgress(int,int)), this, SLOT(reportProgress(int,int))); //setup zoom widget - connect(m_zoomWidget, SIGNAL(levelChanged(int)), m_view, SLOT(zoomTo(int))); + m_zoomWidget->setLevel((View::MaximumZoomLevel+View::MinimumZoomLevel)/2); + connect(m_zoomWidget, SIGNAL(levelChanged(int)), + m_view, SLOT(zoomSliderInput(int))); connect(m_zoomWidget, SIGNAL(zoomInRequest()), m_view, SLOT(zoomIn())); connect(m_zoomWidget, SIGNAL(zoomOutRequest()), m_view, SLOT(zoomOut())); connect(m_view, SIGNAL(zoomLevelChanged(int)), m_zoomWidget, SLOT(setLevel(int))); @@ -66,16 +68,30 @@ connect(m_zoomWidget, SIGNAL(constrainedChanged(bool)), m_view->scene(), SLOT(setConstrained(bool))); connect(m_view->scene(), SIGNAL(constrainedChanged(bool)), m_zoomWidget, SLOT(setConstrained(bool))); //setup widget stack + // /* IDW test. Disable LOADING WIDGET. m_stack->addWidget(m_loadingWidget); + // */ m_stack->addWidget(m_view); + // /* IDW test. Disable LOADING WIDGET. m_stack->setCurrentWidget(m_loadingWidget); + // */ + m_stack->setCurrentWidget(m_view); + //setup layout - QGridLayout* layout = new QGridLayout(this); - layout->addWidget(m_stack, 0, 0, 1, 2); - layout->addWidget(m_progressBar, 1, 0); - layout->addWidget(m_zoomWidget, 1, 1); - layout->setColumnStretch(0, 10); - layout->setMargin(0); + // IDW TODO - Make the background look like a toolbar? Below succeeds, + // but nothing gets painted on it. Try QToolBar::addWidget(). + // QToolBar* pseudoStatusBar = new QToolBar(this); + QWidget* pseudoStatusBar = new QWidget(this); + QHBoxLayout* barLayout = new QHBoxLayout(pseudoStatusBar); + barLayout->addWidget(m_progressBar, 3); // Need not be long. + barLayout->addWidget(m_zoomWidget, 2); // Must hold 200 steps. + barLayout->setContentsMargins(10, 0, 10, 0); // Margins at ends. + + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); // No margins. + mainLayout->setSpacing(0); // No wasted gray space. + mainLayout->addWidget(m_stack, 30); // Max puzzle height. + mainLayout->addWidget(pseudoStatusBar, 1); // Min bar height. } Palapeli::View* Palapeli::PuzzleTableWidget::view() const @@ -95,6 +111,7 @@ void Palapeli::PuzzleTableWidget::reportProgress(int pieceCount, int partCount) { + qDebug() << "PuzzleTableWidget::reportProgress(" << pieceCount << partCount; m_zoomWidget->setEnabled(pieceCount > 0); //zoom does not work reliably when no puzzle is loaded if (m_progressBar->minimum() != 0) m_progressBar->setMinimum(0); @@ -120,7 +137,9 @@ if (pieceCount > 0) m_stack->setCurrentWidget(m_view); else + // ; /* // IDW test. Disable LOADING WIDGET. m_stack->setCurrentWidget(m_loadingWidget); + // */ } void Palapeli::PuzzleTableWidget::setZoomAdjustable(bool adjustable)