diff --git a/src/settings/_default.settings b/src/settings/_default.settings index d3dd485ee..c50397506 100644 --- a/src/settings/_default.settings +++ b/src/settings/_default.settings @@ -578,15 +578,7 @@ "title": "Toggle Razor", "restart": false, "setting": "actionRazorTool", - "value": "C", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Toggle Razor (Alternate 1)", - "restart": false, - "setting": "actionRazorTool", - "value": "B", + "value": "C | B | R", "type": "text" }, { @@ -594,15 +586,7 @@ "title": "Previous Marker", "restart": false, "setting": "actionPreviousMarker", - "value": "Ctrl+Shift+M", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Previous Marker (Alternate 1)", - "restart": false, - "setting": "actionPreviousMarker", - "value": "Ctrl+Left", + "value": "Ctrl+Shift+M | Alt+Left", "type": "text" }, { @@ -610,15 +594,7 @@ "title": "Next Marker", "restart": false, "setting": "actionNextMarker", - "value": "Shift+M", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Next Marker (Alternate 1)", - "restart": false, - "setting": "actionNextMarker", - "value": "Ctrl+Right", + "value": "Shift+M | Alt+Right", "type": "text" }, { @@ -666,15 +642,7 @@ "title": "Export Video", "restart": false, "setting": "actionExportVideo", - "value": "Ctrl+E", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Export Video (Alternate 1)", - "restart": false, - "setting": "actionExportVideo", - "value": "Ctrl+M", + "value": "Ctrl+E | Ctrl+M", "type": "text" }, { @@ -770,15 +738,7 @@ "title": "Zoom In", "restart": false, "setting": "actionTimelineZoomIn", - "value": "=", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Zoom In (Alternate 1)", - "restart": false, - "setting": "actionTimelineZoomIn", - "value": "Ctrl+=", + "value": "= | Ctrl+=", "type": "text" }, { @@ -786,15 +746,7 @@ "title": "Zoom Out", "restart": false, "setting": "actionTimelineZoomOut", - "value": "-", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Zoom Out (Alternate 1)", - "restart": false, - "setting": "actionTimelineZoomOut", - "value": "Ctrl+-", + "value": "- | Ctrl+-", "type": "text" }, { @@ -802,15 +754,7 @@ "title": "Previous Frame", "restart": false, "setting": "seekPreviousFrame", - "value": "Left", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Previous Frame (Alternate 1)", - "restart": false, - "setting": "seekPreviousFrame", - "value": ",", + "value": "Left | ,", "type": "text" }, { @@ -818,15 +762,7 @@ "title": "Next Frame", "restart": false, "setting": "seekNextFrame", - "value": "Right", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Next Frame (Alternate 1)", - "restart": false, - "setting": "seekNextFrame", - "value": ".", + "value": "Right | .", "type": "text" }, { @@ -850,31 +786,7 @@ "title": "Play/Pause Toggle", "restart": false, "setting": "playToggle", - "value": "Space", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Play/Pause Toggle (Alternate 1)", - "restart": false, - "setting": "playToggle1", - "value": "Up", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Play/Pause Toggle (Alternate 2)", - "restart": false, - "setting": "playToggle2", - "value": "Down", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Play/Pause Toggle (Alternate 3)", - "restart": false, - "setting": "playToggle3", - "value": "K", + "value": "Space | Up | Down | K", "type": "text" }, { @@ -882,15 +794,7 @@ "title": "Delete Item", "restart": false, "setting": "deleteItem", - "value": "Delete", - "type": "text" - }, - { - "category": "Keyboard", - "title": "Delete Item (Alternate 1)", - "restart": false, - "setting": "deleteItem1", - "value": "Backspace", + "value": "Delete | Backspace", "type": "text" }, { diff --git a/src/windows/main_window.py b/src/windows/main_window.py index de6fed0d1..416d5d923 100644 --- a/src/windows/main_window.py +++ b/src/windows/main_window.py @@ -1571,7 +1571,7 @@ def actionPreviousMarker_trigger(self, checked=True): all_marker_positions = self.findAllMarkerPositions() # Loop through all markers, and find the closest one to the left - closest_position = Non + closest_position = None for marker_position in sorted(all_marker_positions): # Is marker smaller than position? if marker_position < current_position and (abs(marker_position - current_position) > 0.001): @@ -1682,43 +1682,53 @@ def getAllKeyboardShortcuts(self): return keyboard_shortcuts def initShortcuts(self): - """ Initialize / update QShortcuts for the main window actions """ - # Clear all existing shortcuts for QAction objects - for action in self.findChildren(QAction): - action.setShortcut(QKeySequence()) # Clear any default shortcuts + """Initialize / update QShortcuts for the main window actions.""" - # Dictionary to store all shortcut objects so they don't go out of scope + # Store all QShortcuts so they don't go out of scope if not hasattr(self, 'shortcuts'): - self.shortcuts = {} + self.shortcuts = [] + + # Clear previous QShortcuts + for shortcut in self.shortcuts: + shortcut.setParent(None) # Remove shortcuts by clearing parent + + self.shortcuts.clear() # Clear list of previous shortcuts - # Automatically create shortcuts that follow the pattern self.SHORTCUTNAME or self.SHORTCUTNAME.trigger + # Set to track all key sequences and prevent duplication + used_shortcuts = set() + + # Automatically create shortcuts that follow the pattern self.SHORTCUTNAME for shortcut in self.getAllKeyboardShortcuts(): method_name = shortcut.get('setting') shortcut_value = shortcut.get('value') - # Remove any trailing numbers from the method name + # Split the shortcut values by the pipe '|' and strip any surrounding whitespace + shortcut_sequences = [s.strip() for s in shortcut_value.split('|')] + + # Remove any trailing numbers from the method name (i.e., strip suffix for alternates) base_method_name = re.sub(r'\d+$', '', method_name) - # Check if this method_name refers to a QAction or a method + # Apply the shortcuts to the corresponding QAction or method if hasattr(self, base_method_name): obj = getattr(self, base_method_name) + key_sequences = [QKeySequence(seq) for seq in shortcut_sequences if seq] # Create QKeySequence list + if isinstance(obj, QAction): - # If it's a QAction, set its shortcut - obj.setShortcut(QKeySequence(shortcut_value)) + # Handle QAction with multiple key sequences using setShortcuts() + obj.setShortcuts(key_sequences) else: - # If it's a method or has a trigger, create or update the QShortcut - try: - method = obj.trigger # Check for self.SHORTCUT.trigger - except AttributeError: - method = obj # If no .trigger, assume it's a method - - if method_name in self.shortcuts: - self.shortcuts[method_name].setKey(QKeySequence(shortcut_value)) - else: - self.shortcuts[method_name] = QShortcut(QKeySequence(shortcut_value), self, activated=method, context=Qt.WindowShortcut) + # If it's a method, create QShortcuts for each key sequence + for key_seq_obj in key_sequences: + if key_seq_obj not in used_shortcuts: # Avoid assigning duplicate shortcuts + qshortcut = QShortcut(key_seq_obj, self, activated=obj, context=Qt.WindowShortcut) + self.shortcuts.append(qshortcut) # Keep reference to avoid garbage collection + used_shortcuts.add(key_seq_obj) # Track the shortcut as used + else: + log.warning( + f"Duplicate shortcut {key_seq_obj.toString()} detected for {base_method_name}. Skipping.") else: - log.warning(f"Shortcut {method_name} does not have a matching method or QAction.") + log.warning(f"Shortcut {base_method_name} does not have a matching method or QAction.") # Log shortcut initialization completion log.debug("Shortcuts initialized or updated.") @@ -1879,6 +1889,38 @@ def actionRemoveClip_trigger(self): # Refresh preview get_app().window.refreshFrameSignal.emit() + # def actionInsertKeyframePosition_Triggered(self): + # """Insert a 'Location' / 'Position' keyframe""" + # log.info("Inserting keyframe for position") + # + # def actionInsertKeyframeScale_Triggered(self): + # """Insert a 'Scale' keyframe""" + # log.info("Inserting keyframe for scale") + # + # def actionInsertKeyframeRotation_Triggered(self): + # """Insert a 'Rotation' keyframe""" + # log.info("Inserting keyframe for rotation") + # + # def actionInsertKeyframeAlpha_Triggered(self): + # """Insert an 'Alpha' keyframe""" + # log.info("Inserting keyframe for alpha (opacity)") + # + # def actionRippleDelete_Triggered(self): + # """Removes a clip or transition and shifts the timeline accordingly""" + # log.info("Performing ripple delete") + # + # def actionRippleSelect_Triggered(self): + # """Selects ALL clips or transitions to the right of the current selected item""" + # log.info("Selecting clips for ripple editing") + # + # def actionRippleSliceKeepLeft_Triggered(self): + # """Slice and keep the left side of a clip/transition, and then ripple the position change to the right.""" + # log.info("Slicing timeline and keeping the left side") + # + # def actionRippleSliceKeepRight_Triggered(self): + # """Slice and keep the right side of a clip/transition, and then ripple the position change to the right.""" + # log.info("Slicing timeline and keeping the right side") + def actionProperties_trigger(self): log.debug('actionProperties_trigger') @@ -3103,19 +3145,30 @@ def copyAll(self): """Handle Copy QShortcut (selected clips / transitions)""" self.timeline.Copy_Triggered(MenuCopy.ALL, self.selected_clips, self.selected_transitions, []) + def cutAll(self): + """Copy and remove the currently selected clip/transition""" + self.copyAll() + self.deleteItem() + + def pasteAll(self): + """Handle Paste QShortcut (at timeline position, same track as original clip)""" + self.timeline.Paste_Triggered(MenuCopy.PASTE, self.selected_clips, self.selected_transitions) + def nudgeLeft(self): """Nudge the selected clips to the left""" self.timeline.Nudge_Triggered(-1, self.selected_clips, self.selected_transitions) + def nudgeLeftBig(self): + """Nudge the selected clip/transition to the left (5 pixels)""" + self.timeline.Nudge_Triggered(-5, self.selected_clips, self.selected_transitions) + def nudgeRight(self): """Nudge the selected clips to the right""" self.timeline.Nudge_Triggered(1, self.selected_clips, self.selected_transitions) - def pasteAll(self): - """Handle Paste QShortcut (at timeline position, same track as original clip)""" - fps = get_app().project.get("fps") - fps_float = float(fps["num"]) / float(fps["den"]) - self.timeline.Paste_Triggered(MenuCopy.PASTE, self.selected_clips, self.selected_transitions) + def nudgeRightBig(self): + """Nudge the selected clip/transition to the right (5 pixels)""" + self.timeline.Nudge_Triggered(5, self.selected_clips, self.selected_transitions) def eventFilter(self, obj, event): """Filter out certain QShortcuts - for example, arrow keys used diff --git a/src/windows/views/timeline.py b/src/windows/views/timeline.py index 4636d96ad..f0dc59a05 100644 --- a/src/windows/views/timeline.py +++ b/src/windows/views/timeline.py @@ -1779,90 +1779,33 @@ def apply_clipboard_data(target_obj, clipboard_data, excluded_keys=None): .format(local_mouse_pos.x(), local_mouse_pos.y()), partial(callback, self, clip_ids, tran_ids)) def Nudge_Triggered(self, action, clip_ids, tran_ids): - """Callback for clip nudges""" - log.debug("Nudging clip(s) and/or transition(s)") - left_edge = -1.0 - right_edge = -1.0 - - # Determine how far we're going to nudge (1/2 frame or 0.01s, whichever is larger) + """Callback for nudging clips/transitions by a specified number of frames.""" + # Determine the nudge duration in seconds based on the FPS fps = get_app().project.get("fps") fps_float = float(fps["num"]) / float(fps["den"]) - nudgeDistance = float(action) / float(fps_float) - nudgeDistance /= 2.0 # 1/2 frame - if abs(nudgeDistance) < 0.01: - nudgeDistance = 0.01 * action # nudge is less than the minimum of +/- 0.01s - log.debug("Nudging by %s sec" % nudgeDistance) + nudge_duration = float(action) / fps_float # Nudge duration in seconds + log.debug(f"Nudging by {nudge_duration} seconds") - # Loop through each selected clip (find furthest left and right edge) + # Nudge all selected clips for clip_id in clip_ids: - # Get existing clip object clip = Clip.get(id=clip_id) if not clip: - # Invalid clip, skip to next item continue - position = float(clip.data["position"]) - start_of_clip = float(clip.data["start"]) - end_of_clip = float(clip.data["end"]) - - if position < left_edge or left_edge == -1.0: - left_edge = position - if position + (end_of_clip - start_of_clip) > right_edge or right_edge == -1.0: - right_edge = position + (end_of_clip - start_of_clip) - - # Do not nudge beyond the start of the timeline - if left_edge + nudgeDistance < 0.0: - log.info("Cannot nudge beyond start of timeline") - nudgeDistance = 0 - - # Loop through each selected transition (find furthest left and right edge) - for tran_id in tran_ids: - # Get existing transition object - tran = Transition.get(id=tran_id) - if not tran: - # Invalid transition, skip to next item - continue - - position = float(tran.data["position"]) - start_of_tran = float(tran.data["start"]) - end_of_tran = float(tran.data["end"]) - - if position < left_edge or left_edge == -1.0: - left_edge = position - if position + (end_of_tran - start_of_tran) > right_edge or right_edge == -1.0: - right_edge = position + (end_of_tran - start_of_tran) - - # Do not nudge beyond the start of the timeline - if left_edge + nudgeDistance < 0.0: - log.info("Cannot nudge beyond start of timeline") - nudgeDistance = 0 - - # Loop through each selected clip (update position to align clips) - for clip_id in clip_ids: - # Get existing clip object - clip = Clip.get(id=clip_id) - if not clip: - # Invalid clip, skip to next item - continue - - # Do the nudge - clip.data['position'] += nudgeDistance - - # Save changes + # Apply the nudge and ensure the position doesn't go below 0 + new_position = max(clip.data['position'] + nudge_duration, 0.0) + clip.data['position'] = new_position self.update_clip_data(clip.data, only_basic_props=False, ignore_reader=True) - # Loop through each selected transition (update position to align clips) + # Nudge all selected transitions for tran_id in tran_ids: - # Get existing transition object tran = Transition.get(id=tran_id) if not tran: - # Invalid transition, skip to next item continue - # Do the nudge - tran.data['position'] += nudgeDistance - - # Save changes + # Apply the nudge and ensure the position doesn't go below 0 + new_position = max(tran.data['position'] + nudge_duration, 0.0) + tran.data['position'] = new_position self.update_transition_data(tran.data, only_basic_props=False) def Align_Triggered(self, action, clip_ids, tran_ids):