diff --git a/js/actionCreators.js b/js/actionCreators.js index 2e8ad3c5..775782db 100644 --- a/js/actionCreators.js +++ b/js/actionCreators.js @@ -16,7 +16,10 @@ import { getPlaylistURL, getSelectedTrackObjects, getTracks, - getTrackIsVisibleFunction + getTrackIsVisibleFunction, + getWindowGraph, + getWindowPositions, + getWindowSizes } from "./selectors"; import { @@ -59,10 +62,14 @@ import { TOGGLE_PLAYLIST_SHADE_MODE, MEDIA_TAG_REQUEST_INITIALIZED, MEDIA_TAG_REQUEST_FAILED, - PLAYLIST_SIZE_CHANGED + PLAYLIST_SIZE_CHANGED, + UPDATE_WINDOW_POSITIONS, + TOGGLE_DOUBLESIZE_MODE } from "./actionTypes"; import LoadQueue from "./loadQueue"; +import { getPositionDiff } from "./resizeUtils"; +import { applyDiff } from "./snapUtils"; // Lower is better const DURATION_VISIBLE_PRIORITY = 5; @@ -469,16 +476,57 @@ export function downloadPreset() { }; } +// Dispatch an action and, if needed rearrange the windows to preserve +// the existing edge relationship. +// +// Works by checking the edges before the action is dispatched. Then, +// after disatching, calculating what position change would be required +// to restore those relationships. +function withWindowGraphIntegrity(action) { + return (dispatch, getState) => { + const state = getState(); + const graph = getWindowGraph(state); + const originalSizes = getWindowSizes(state); + + dispatch(action); + + const newSizes = getWindowSizes(getState()); + const sizeDiff = {}; + for (const window of Object.keys(newSizes)) { + const original = originalSizes[window]; + const current = newSizes[window]; + sizeDiff[window] = { + height: current.height - original.height, + width: current.width - original.width + }; + } + + const positionDiff = getPositionDiff(graph, sizeDiff); + const windowPositions = getWindowPositions(state); + + const newPositions = {}; + for (const key of Object.keys(windowPositions)) { + newPositions[key] = applyDiff(windowPositions[key], positionDiff[key]); + } + + dispatch(updateWindowPositions(newPositions)); + }; +} + +export function toggleDoubleSizeMode() { + return withWindowGraphIntegrity({ type: TOGGLE_DOUBLESIZE_MODE }); +} + export function toggleEqualizerShadeMode() { - return { type: TOGGLE_EQUALIZER_SHADE_MODE }; + return withWindowGraphIntegrity({ type: TOGGLE_EQUALIZER_SHADE_MODE }); } export function toggleMainWindowShadeMode() { - return { type: TOGGLE_MAIN_SHADE_MODE }; + return withWindowGraphIntegrity({ type: TOGGLE_MAIN_SHADE_MODE }); } export function togglePlaylistShadeMode() { - return { type: TOGGLE_PLAYLIST_SHADE_MODE }; + return withWindowGraphIntegrity({ type: TOGGLE_PLAYLIST_SHADE_MODE }); } export function closeEqualizerWindow() { @@ -625,3 +673,7 @@ export function downloadHtmlPlaylist() { downloadURI(uri, "Winamp Playlist.html"); }; } + +export function updateWindowPositions(positions) { + return { type: UPDATE_WINDOW_POSITIONS, positions }; +} diff --git a/js/actionTypes.js b/js/actionTypes.js index 3fdb2ee7..25f833c7 100644 --- a/js/actionTypes.js +++ b/js/actionTypes.js @@ -64,3 +64,4 @@ export const MEDIA_TAG_REQUEST_INITIALIZED = "MEDIA_TAG_REQUEST_INITIALIZED"; export const MEDIA_TAG_REQUEST_FAILED = "MEDIA_TAG_REQUEST_FAILED"; export const NETWORK_CONNECTED = "NETWORK_CONNECTED"; export const NETWORK_DISCONNECTED = "NETWORK_DISCONNECTED"; +export const UPDATE_WINDOW_POSITIONS = "UPDATE_WINDOW_POSITIONS"; diff --git a/js/components/MainWindow/ClutterBar.js b/js/components/MainWindow/ClutterBar.js index 5566db71..9272b7bf 100644 --- a/js/components/MainWindow/ClutterBar.js +++ b/js/components/MainWindow/ClutterBar.js @@ -2,11 +2,8 @@ import React from "react"; import { connect } from "react-redux"; import classnames from "classnames"; -import { - SET_FOCUS, - TOGGLE_DOUBLESIZE_MODE, - UNSET_FOCUS -} from "../../actionTypes"; +import { SET_FOCUS, UNSET_FOCUS } from "../../actionTypes"; +import { toggleDoubleSizeMode } from "../../actionCreators"; const ClutterBar = props => (
@@ -31,7 +28,7 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ handleMouseDown: () => dispatch({ type: SET_FOCUS, input: "double" }), handleMouseUp: () => { - dispatch({ type: TOGGLE_DOUBLESIZE_MODE }); + dispatch(toggleDoubleSizeMode()); dispatch({ type: UNSET_FOCUS }); } }); diff --git a/js/components/PlaylistWindow/PlaylistShade.js b/js/components/PlaylistWindow/PlaylistShade.js index 5e7a2046..3fa343f7 100644 --- a/js/components/PlaylistWindow/PlaylistShade.js +++ b/js/components/PlaylistWindow/PlaylistShade.js @@ -3,11 +3,7 @@ import { connect } from "react-redux"; import classnames from "classnames"; import { getOrderedTracks, getMinimalMediaText } from "../../selectors"; import { getTimeStr } from "../../utils"; -import { - TOGGLE_PLAYLIST_WINDOW, - TOGGLE_PLAYLIST_SHADE_MODE, - SET_FOCUSED_WINDOW -} from "../../actionTypes"; +import { TOGGLE_PLAYLIST_WINDOW, SET_FOCUSED_WINDOW } from "../../actionTypes"; import { WINDOWS, @@ -16,6 +12,7 @@ import { CHARACTER_WIDTH, UTF8_ELLIPSIS } from "../../constants"; +import { togglePlaylistShadeMode } from "../../actionCreators"; import CharacterString from "../CharacterString"; import PlaylistResizeTarget from "./PlaylistResizeTarget"; @@ -83,7 +80,7 @@ const mapDispatchToProps = dispatch => ({ focusPlaylist: () => dispatch({ type: SET_FOCUSED_WINDOW, window: WINDOWS.PLAYLIST }), close: () => dispatch({ type: TOGGLE_PLAYLIST_WINDOW }), - toggleShade: () => dispatch({ type: TOGGLE_PLAYLIST_SHADE_MODE }) + toggleShade: () => dispatch(togglePlaylistShadeMode()) }); const mapStateToProps = state => { diff --git a/js/components/PlaylistWindow/index.js b/js/components/PlaylistWindow/index.js index a40ff779..924b9bea 100644 --- a/js/components/PlaylistWindow/index.js +++ b/js/components/PlaylistWindow/index.js @@ -2,18 +2,8 @@ import React from "react"; import { connect } from "react-redux"; import classnames from "classnames"; -import { - WINDOWS, - PLAYLIST_RESIZE_SEGMENT_WIDTH, - PLAYLIST_RESIZE_SEGMENT_HEIGHT, - MIN_PLAYLIST_WINDOW_WIDTH, - TRACK_HEIGHT -} from "../../constants"; -import { - TOGGLE_PLAYLIST_WINDOW, - TOGGLE_PLAYLIST_SHADE_MODE, - SET_FOCUSED_WINDOW -} from "../../actionTypes"; +import { WINDOWS, TRACK_HEIGHT } from "../../constants"; +import { TOGGLE_PLAYLIST_WINDOW, SET_FOCUSED_WINDOW } from "../../actionTypes"; import { toggleVisualizerStyle, scrollUpFourTracks, @@ -22,7 +12,7 @@ import { togglePlaylistShadeMode, scrollVolume } from "../../actionCreators"; -import { getScrollOffset } from "../../selectors"; +import { getScrollOffset, getPlaylistWindowPixelSize } from "../../selectors"; import { clamp } from "../../utils"; import DropTarget from "../DropTarget"; @@ -39,8 +29,6 @@ import ScrollBar from "./ScrollBar"; import "../../../css/playlist-window.css"; -const MIN_WINDOW_HEIGHT = 116; - class PlaylistWindow extends React.Component { constructor(props) { super(props); @@ -63,6 +51,7 @@ class PlaylistWindow extends React.Component { focusPlaylist, focused, playlistSize, + playlistWindowPixelSize, playlistShade, close, toggleShade @@ -75,10 +64,8 @@ class PlaylistWindow extends React.Component { color: skinPlaylistStyle.normal, backgroundColor: skinPlaylistStyle.normalbg, fontFamily: `${skinPlaylistStyle.font}, Arial, sans-serif`, - height: `${MIN_WINDOW_HEIGHT + - playlistSize[1] * PLAYLIST_RESIZE_SEGMENT_HEIGHT}px`, - width: `${MIN_PLAYLIST_WINDOW_WIDTH + - playlistSize[0] * PLAYLIST_RESIZE_SEGMENT_WIDTH}px` + height: `${playlistWindowPixelSize.height}px`, + width: `${playlistWindowPixelSize.width}px` }; const classes = classnames("window", "draggable", { @@ -162,7 +149,7 @@ const mapDispatchToProps = { window: WINDOWS.PLAYLIST }), close: () => ({ type: TOGGLE_PLAYLIST_WINDOW }), - toggleShade: () => ({ type: TOGGLE_PLAYLIST_SHADE_MODE }), + toggleShade: togglePlaylistShadeMode, toggleVisualizerStyle, scrollUpFourTracks, scrollDownFourTracks, @@ -183,6 +170,7 @@ const mapStateToProps = state => { return { offset: getScrollOffset(state), maxTrackIndex: trackOrder.length - 1, + playlistWindowPixelSize: getPlaylistWindowPixelSize(state), focused, skinPlaylistStyle, playlistSize, diff --git a/js/components/WindowManager.js b/js/components/WindowManager.js index db991f8b..0526859c 100644 --- a/js/components/WindowManager.js +++ b/js/components/WindowManager.js @@ -1,5 +1,6 @@ import React from "react"; import PropTypes from "prop-types"; +import { connect } from "react-redux"; import { snapDiffManyToMany, @@ -10,6 +11,8 @@ import { applyDiff, applyMultipleDiffs } from "../snapUtils"; +import { getWindowPositions } from "../selectors"; +import { updateWindowPositions } from "../actionCreators"; const WINDOW_HEIGHT = 116; const WINDOW_WIDTH = 275; @@ -25,7 +28,6 @@ class WindowManager extends React.Component { constructor(props) { super(props); this.windowNodes = {}; - this.state = {}; this.getRef = this.getRef.bind(this); this.handleMouseDown = this.handleMouseDown.bind(this); this.centerWindows = this.centerWindows.bind(this); @@ -62,17 +64,17 @@ class WindowManager extends React.Component { width = container.scrollWidth; height = container.scrollHeight; } - const state = {}; + const windowPositions = {}; const keys = this.windowKeys(); const totalHeight = keys.length * WINDOW_HEIGHT; keys.forEach((key, i) => { const offset = WINDOW_HEIGHT * i; - state[key] = { - left: offsetLeft + (width / 2 - WINDOW_WIDTH / 2), - top: offsetTop + (height / 2 - totalHeight / 2 + offset) + windowPositions[key] = { + x: offsetLeft + (width / 2 - WINDOW_WIDTH / 2), + y: offsetTop + (height / 2 - totalHeight / 2 + offset) }; }); - this.setState(state); + this.props.updateWindowPositions(windowPositions); } getRef(key, node) { @@ -154,16 +156,12 @@ class WindowManager extends React.Component { const finalDiff = applyMultipleDiffs(proposedDiff, snapDiff, withinDiff); - const stateDiff = moving.reduce((diff, window) => { - const newWindowLocation = applyDiff(window, finalDiff); - diff[window.key] = { - top: newWindowLocation.y, - left: newWindowLocation.x - }; + const windowPositionDiff = moving.reduce((diff, window) => { + diff[window.key] = applyDiff(window, finalDiff); return diff; }, {}); - this.setState(stateDiff); + this.props.updateWindowPositions(windowPositionDiff); }; const removeListeners = () => { @@ -198,13 +196,13 @@ class WindowManager extends React.Component { return (
{this.windowKeys().map(key => { - const position = this.state[key]; + const position = this.props.windowPositions[key]; return ( position && (
this.handleMouseDown(key, e)} ref={node => this.getRef(key, node)} - style={{ ...style, ...position }} + style={{ ...style, left: position.x, top: position.y }} key={key} > {this.props.windows[key]} @@ -222,4 +220,12 @@ WindowManager.propTypes = { container: PropTypes.instanceOf(Element) }; -export default WindowManager; +const mapStateToProps = state => ({ + windowPositions: getWindowPositions(state) +}); + +const mapDispatchToProps = { + updateWindowPositions +}; + +export default connect(mapStateToProps, mapDispatchToProps)(WindowManager); diff --git a/js/hotkeys.js b/js/hotkeys.js index 9629b4a9..6d85f913 100644 --- a/js/hotkeys.js +++ b/js/hotkeys.js @@ -11,14 +11,11 @@ import { reverseList, nextN, next, - previous + previous, + toggleDoubleSizeMode } from "./actionCreators"; -import { - TOGGLE_DOUBLESIZE_MODE, - TOGGLE_TIME_MODE, - TOGGLE_LLAMA_MODE -} from "./actionTypes"; +import { TOGGLE_TIME_MODE, TOGGLE_LLAMA_MODE } from "./actionTypes"; import { arraysAreEqual } from "./utils"; @@ -39,7 +36,7 @@ export default function(dispatch) { // Is CTRL depressed? switch (e.keyCode) { case 68: // CTRL+D - dispatch({ type: TOGGLE_DOUBLESIZE_MODE }); + dispatch(toggleDoubleSizeMode()); e.preventDefault(); // Supress the "Bookmark" action on windows. break; case 76: // CTRL+L FIXME diff --git a/js/reducers/windows.js b/js/reducers/windows.js index 5240abd1..8492ac74 100644 --- a/js/reducers/windows.js +++ b/js/reducers/windows.js @@ -5,7 +5,8 @@ import { CLOSE_EQUALIZER_WINDOW, TOGGLE_PLAYLIST_WINDOW, CLOSE_GEN_WINDOW, - OPEN_GEN_WINDOW + OPEN_GEN_WINDOW, + UPDATE_WINDOW_POSITIONS } from "../actionTypes"; import { arrayWith, arrayWithout } from "../utils"; @@ -15,7 +16,8 @@ const defaultWindowsState = { equalizer: true, playlist: true, // openGenWindows: ["AVS_WINDOW"] - openGenWindows: [] + openGenWindows: [], + positions: {} }; const windows = (state = defaultWindowsState, action) => { @@ -38,6 +40,11 @@ const windows = (state = defaultWindowsState, action) => { ...state, openGenWindows: arrayWith(state.openGenWindow, action.windowId) }; + case UPDATE_WINDOW_POSITIONS: + return { + ...state, + positions: { ...state.positions, ...action.positions } + }; default: return state; } diff --git a/js/selectors.js b/js/selectors.js index 9fbbe2e5..c622e64a 100644 --- a/js/selectors.js +++ b/js/selectors.js @@ -2,11 +2,14 @@ import { createSelector } from "reselect"; import { denormalize, getTimeStr, clamp, percentToIndex } from "./utils"; import { BANDS, + TRACK_HEIGHT, + PLAYLIST_RESIZE_SEGMENT_WIDTH, PLAYLIST_RESIZE_SEGMENT_HEIGHT, - TRACK_HEIGHT + MIN_PLAYLIST_WINDOW_WIDTH } from "./constants"; import { createPlaylistURL } from "./playlistHtml"; import * as fromPlaylist from "./reducers/playlist"; +import { generateGraph } from "./resizeUtils"; export const getEqfData = state => { const { sliders } = state.equalizer; @@ -228,3 +231,61 @@ export const getPlaylistURL = createSelector( ) }) ); + +export function getWindowPositions(state) { + return state.windows.positions; +} + +const WINDOW_WIDTH = 275; +const WINDOW_HEIGHT = 116; +const DEFAUT_WINDOW_SIZE = { + height: WINDOW_HEIGHT, + width: WINDOW_WIDTH +}; +const SHADE_WINDOW_HEIGHT = 14; + +export function getPlaylistWindowPixelSize(state) { + const { playlistSize } = state.display; + return { + height: WINDOW_HEIGHT + playlistSize[1] * PLAYLIST_RESIZE_SEGMENT_HEIGHT, + width: + MIN_PLAYLIST_WINDOW_WIDTH + + playlistSize[0] * PLAYLIST_RESIZE_SEGMENT_WIDTH + }; +} + +function getGenericWindowSize(size, shade, doubled) { + const doubledMultiplier = doubled ? 2 : 1; + return { + height: (shade ? SHADE_WINDOW_HEIGHT : size.height) * doubledMultiplier, + width: size.width * doubledMultiplier + }; +} + +export function getWindowSizes(state) { + const { doubled, mainShade, equalizerShade, playlistShade } = state.display; + const main = getGenericWindowSize(DEFAUT_WINDOW_SIZE, mainShade, doubled); + const equalizer = getGenericWindowSize( + DEFAUT_WINDOW_SIZE, + equalizerShade, + doubled + ); + const playlist = getGenericWindowSize( + getPlaylistWindowPixelSize(state), + playlistShade, + false // The playlist cannot be doubled + ); + return { main, equalizer, playlist }; +} + +export const getWindowGraph = createSelector( + getWindowPositions, + getWindowSizes, + (windowPositions, windowSizes) => { + const windowData = []; + for (const key of Object.keys(windowPositions)) { + windowData.push({ key, ...windowPositions[key], ...windowSizes[key] }); + } + return generateGraph(windowData); + } +);