From 962b46ec6e33405526c3f7be7ff01b7f35af334e Mon Sep 17 00:00:00 2001 From: Marcin Zawalski Date: Sat, 24 Oct 2026 13:18:05 +0200 Subject: [PATCH] feat(ui): mirror auto helpers on tab headers Exposure tab header gets the Auto wand beside Pick Better; it opens the Tone card's own Auto menu, so both stay in step. Geometry header mirrors Auto Crop and Square to Film; Finish header mirrors Optical Removal and IR Removal (hidden when the scan has no infrared channel). TabHeader.add_mirror forwards clicks to the card button and follows its check, enabled state and explicit hide, also while the card is collapsed. IR gets one icon (mdi6.heat-wave) on IR Removal, the Scan IR toggle and the IR choice; ICE keeps the broom. --- docs/USER_GUIDE.md | 10 ++-- negpy/desktop/view/sidebar/controls_panel.py | 23 ++++++++ negpy/desktop/view/sidebar/retouch.py | 2 +- negpy/desktop/view/sidebar/scan.py | 4 +- negpy/desktop/view/widgets/tab_header.py | 39 ++++++++++++- negpy/translations/USER_GUIDE.en.md | 10 ++-- negpy/translations/USER_GUIDE.ja.md | 18 +++--- negpy/translations/USER_GUIDE.ko.md | 18 +++--- negpy/translations/USER_GUIDE.zh_CN.md | 18 +++--- tests/test_tab_header.py | 60 ++++++++++++++++++++ 10 files changed, 160 insertions(+), 42 deletions(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f5b6b6cfd..b57a974b6 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -223,7 +223,7 @@ Two cards: **Crop** sets the part of the picture you keep, and **Straighten** tu Choose the part of the picture to keep, by hand or with Auto Crop. * **Draw**: drag a crop on the picture. Drag a corner to resize it, the inside to move it, or outside the box to rotate. `Enter` or **Done** keeps the crop, and `Esc` puts back the crop you had before. The **×** removes a crop you drew. Drawing turns Auto Crop off. -* **Auto Crop**: finds the edges of the picture and crops to them. +* **Auto Crop** (also on the Geometry tab's bar): finds the edges of the picture and crops to them. * **Ratio**: the crop shape, such as `3:2` or `1:1`, turned to fit the frame. **Free** uses the shape NegPy finds, and you can drag one side at a time. **Detect** picks the nearest standard shape. * **Guide**: composition lines, such as thirds, while you draw. The rotate button turns them. * **Mode** (Auto Crop Settings fold): **Image only** crops to the picture. **Film edge** keeps the whole film with its border and holes. @@ -240,7 +240,7 @@ Turn the picture and make it level. * **Rotate** and **Flip**: turn the picture a quarter turn, or mirror it. The toolbar has the same buttons. * **Level**: draw a line along the horizon or a vertical edge, and the picture turns to make it level. -* **Square to Film**: lines the picture up with the edges of the film. It ignores what is in the picture, so a photo taken at a tilt stays tilted. +* **Square to Film** (also on the Geometry tab's bar): lines the picture up with the edges of the film. It ignores what is in the picture, so a photo taken at a tilt stays tilted. * **Fine Rotation**: turns the picture by small amounts. Positive is clockwise. * **Tilt** and **Swing** (fold): straighten a building shot from below (Tilt) or a wall shot from one side (Swing). **Draw Lines** sets both from lines along the four film edges. * **Crop Empty Corners**: crops away the empty corners that rotation, Tilt and Swing leave. @@ -271,7 +271,7 @@ How light or dark the print is, and how much contrast it has. * **Print Density**: how light or dark the print is (the darkroom's exposure time). Lower is lighter. * **ISO-R Grade**: contrast (the darkroom's paper grade). A lower number gives more contrast; about 110 is normal. -* **Auto**: **Auto Density** and **Auto Grade** set brightness and contrast for each frame, so a roll prints evenly. A slider then shows the automatic value: move it to adjust, double-click it to go back. The Auto menu also has: +* **Auto** (the wand, also on the Exposure tab's bar): **Auto Density** and **Auto Grade** set brightness and contrast for each frame, so a roll prints evenly. A slider then shows the automatic value: move it to adjust, double-click it to go back. The Auto menu also has: * **Set Targets…**: the brightness and contrast Auto aims for. * **Bake Auto Density/Grade**: keeps this frame's automatic values and turns Auto off for it. * **Hold Black**: when you lighten the shadows, the deepest blacks stay black. @@ -397,8 +397,8 @@ On a lith print only Selenium and Gold work. On a cyanotype no toner works. Removes dust, hairs and scratches. Each repair is rebuilt from the clean film around it. * **Overlay**: **Marked** shows what dust removal found, **IR** the scan's infrared channel. -* **Optical Removal**: finds dust and hairs by itself. **Spot Threshold** and **Hair Threshold**: lower finds more but can remove real detail; 1 is off. **Size**: the largest dust to remove. To protect an area, right-drag over it or right-click a mark; the cursor button skips the right-click menu. -* **IR Removal** (scans with an infrared channel): finds dust in infrared, where the picture does not show. **IR Threshold**: lower finds more. B&W and Kodachrome are skipped: their picture shows in infrared too. The channel comes from 4-channel TIFF and DNG scans or an `_IR.tif` file beside the scan. +* **Optical Removal** (also on the Finish tab's bar): finds dust and hairs by itself. **Spot Threshold** and **Hair Threshold**: lower finds more but can remove real detail; 1 is off. **Size**: the largest dust to remove. To protect an area, right-drag over it or right-click a mark; the cursor button skips the right-click menu. +* **IR Removal** (scans with an infrared channel, also on the Finish tab's bar): finds dust in infrared, where the picture does not show. **IR Threshold**: lower finds more. B&W and Kodachrome are skipped: their picture shows in infrared too. The channel comes from 4-channel TIFF and DNG scans or an `_IR.tif` file beside the scan. * **Heal**: click or paint over dust. Only marks that stand out are repaired, so paint generously. * **Scratch**: click points along a scratch or hair, then press Enter. * **Line**: click once on a long straight scratch, and NegPy finds and repairs the whole line. **Line Sensitivity**: lower finds fainter lines. diff --git a/negpy/desktop/view/sidebar/controls_panel.py b/negpy/desktop/view/sidebar/controls_panel.py index 223829ea0..5e3921398 100644 --- a/negpy/desktop/view/sidebar/controls_panel.py +++ b/negpy/desktop/view/sidebar/controls_panel.py @@ -1,4 +1,5 @@ from negpy.kernel.system.i18n import tr +import qtawesome as qta from PyQt6.QtWidgets import ( QMenu, QWidget, @@ -422,6 +423,16 @@ def _init_ui(self) -> None: page_layout.addWidget(header) if key == "tone": self._add_pick_better(header) + self._add_auto_mirror(header) + elif key == "geometry": + self.auto_crop_mirror_btn = header.add_mirror(self.autocrop_sidebar.auto_frame_btn) + self.square_to_film_mirror_btn = header.add_mirror( + self.geometry_sidebar.auto_skew_btn, + qta.icon("fa5s.ruler-combined", color=THEME.text_hint, color_disabled=THEME.text_muted), + ) + elif key == "finish": + self.optical_removal_mirror_btn = header.add_mirror(self.retouch_sidebar.auto_dust_btn) + self.ir_removal_mirror_btn = header.add_mirror(self.retouch_sidebar.ir_dust_btn) for section in sections: page_layout.addWidget(section) page_layout.addStretch(1) @@ -453,6 +464,15 @@ def _add_pick_better(self, header: TabHeader) -> None: # Session state, not config, so the button follows the controller rather than sync_ui. self.controller.pick_better_changed.connect(self._sync_pick_better_btn) + def _add_auto_mirror(self, header: TabHeader) -> None: + """The Tone card's Auto button again on the tab's header: the one menu, so both stay in step.""" + auto = self.tone_sidebar.auto_btn + icon = qta.icon("fa5s.magic", color=THEME.text_hint, color_on=THEME.text_primary, color_disabled=THEME.text_muted) + self.auto_mirror_btn = header.add_lead_toggle(icon, auto.plain_tooltip) + self.auto_mirror_btn.setMenu(auto.choice_menu) + self.auto_mirror_btn.setChecked(auto.isChecked()) + auto.toggled.connect(self.auto_mirror_btn.setChecked) + @staticmethod def _pick_better_tooltip(printing: bool = False) -> str: if printing: @@ -566,6 +586,9 @@ def apply_shortcut_tooltips(self) -> None: (self.retouch_sidebar.auto_dust_btn, "toggle_optical_removal"), (self.retouch_sidebar.right_click_btn, "toggle_right_click_excludes"), (self.retouch_sidebar.ir_dust_btn, "toggle_ir_removal"), + (self.optical_removal_mirror_btn, "toggle_optical_removal"), + (self.ir_removal_mirror_btn, "toggle_ir_removal"), + (self.auto_crop_mirror_btn, "auto_crop"), (self.flatfield_sidebar.enable_btn, "toggle_flat_field"), (self.flatfield_sidebar.check_btn, "toggle_flatfield_peek"), (self.autocrop_sidebar.auto_frame_btn, "auto_crop"), diff --git a/negpy/desktop/view/sidebar/retouch.py b/negpy/desktop/view/sidebar/retouch.py index f0a0e9299..2bc5c77bf 100644 --- a/negpy/desktop/view/sidebar/retouch.py +++ b/negpy/desktop/view/sidebar/retouch.py @@ -51,7 +51,7 @@ def _init_ui(self) -> None: self.optical_rail = SliderGroup(self.threshold_slider, self.hair_threshold_slider, self.auto_size_slider) self.layout.addWidget(self.optical_rail) - self.ir_dust_btn = self._small_toggle("fa5s.broom", tr("IR Removal"), conf.ir_dust_remove, _IR_REMOVAL_TIP, align_left=True) + self.ir_dust_btn = self._small_toggle("mdi6.heat-wave", tr("IR Removal"), conf.ir_dust_remove, _IR_REMOVAL_TIP, align_left=True) self.layout.addWidget(self.ir_dust_btn) self.ir_threshold_slider = CompactSlider(tr("IR Threshold"), 0.05, 0.95, float(conf.ir_threshold)) self.ir_rail = SliderGroup(self.ir_threshold_slider) diff --git a/negpy/desktop/view/sidebar/scan.py b/negpy/desktop/view/sidebar/scan.py index c1b394afc..ab814136f 100644 --- a/negpy/desktop/view/sidebar/scan.py +++ b/negpy/desktop/view/sidebar/scan.py @@ -332,11 +332,11 @@ def _init_ui(self) -> None: self.samples_widget = self._row_widget(field_row(tr("Samples"), self.samples_btn)) quality.addWidget(self.samples_widget) - self.ir_btn = tool_toggle("fa5s.layer-group", tr("IR"), tr("Scan a separate infrared channel for dust detection")) + self.ir_btn = tool_toggle("mdi6.heat-wave", tr("IR"), tr("Scan a separate infrared channel for dust detection")) # IR and ICE read the same pass, and ICE bakes its repair into the file, so a raw IR # plane beside it would only have cleaned pixels to detect dust in: one choice of three. self.dust_btn = SegmentedChoice( - (("", tr("Off")), ("fa5s.layer-group", tr("IR")), ("fa5s.broom", tr("ICE"))), + (("", tr("Off")), ("mdi6.heat-wave", tr("IR")), ("fa5s.broom", tr("ICE"))), tr("How the scan uses the infrared pass against dust: not at all, as a separate channel, or as ICE in the scanner."), data=("off", "ir", "ice"), ) diff --git a/negpy/desktop/view/widgets/tab_header.py b/negpy/desktop/view/widgets/tab_header.py index cfb602d21..b8bea8deb 100644 --- a/negpy/desktop/view/widgets/tab_header.py +++ b/negpy/desktop/view/widgets/tab_header.py @@ -7,9 +7,9 @@ from typing import Iterable import qtawesome as qta -from PyQt6.QtCore import pyqtSignal +from PyQt6.QtCore import QEvent, QObject, pyqtSignal from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QPushButton +from PyQt6.QtWidgets import QAbstractButton, QPushButton from negpy.desktop.view.confirm import confirm_reset_tab from negpy.desktop.view.styles.templates import header_button, wrap_tooltip @@ -57,6 +57,18 @@ def add_lead_toggle(self, icon: QIcon, tooltip: str) -> QPushButton: self.icon_label.hide() return btn + def add_mirror(self, source: QAbstractButton, icon: QIcon | None = None) -> QPushButton: + """A lead button that clicks `source`, a card's own button, and follows its check, + its enabled state and whether the card hides it. `icon` replaces an icon drawn for a + labeled button, which is always lit.""" + btn = self.add_lead_toggle(icon or source.icon(), source.toolTip()) + btn.plain_tooltip = getattr(source, "plain_tooltip", "") + btn.setCheckable(source.isCheckable()) + btn.clicked.connect(lambda: (source.click(), btn.setChecked(source.isChecked()))) + source.toggled.connect(btn.setChecked) + _MirrorSync(source, btn) + return btn + def bind(self, sections: Iterable[CollapsibleSection]) -> None: self._sections = tuple(sections) self.refresh() @@ -96,3 +108,26 @@ def _sync_cards_button(self) -> None: collapse = self._any_expanded() self.cards_btn.setIcon(qta.icon("fa5s.angle-double-up" if collapse else "fa5s.angle-double-down", color=THEME.text_hint)) self.cards_btn.setToolTip(wrap_tooltip(tr("Collapse every card on this tab") if collapse else tr("Expand every card on this tab"))) + + +class _MirrorSync(QObject): + """Copies a source button's explicit hide and enabled state to its mirror. ShowToParent and + HideToParent arrive even while a collapsed card keeps the source off screen.""" + + _EVENTS = (QEvent.Type.ShowToParent, QEvent.Type.HideToParent, QEvent.Type.EnabledChange) + + def __init__(self, source: QAbstractButton, mirror: QPushButton): + super().__init__(mirror) + self._source, self._mirror = source, mirror + source.installEventFilter(self) + self._sync() + + def eventFilter(self, obj, event) -> bool: # noqa: N802 + if event.type() in self._EVENTS: + self._sync() + return False + + def _sync(self) -> None: + self._mirror.setVisible(not self._source.isHidden()) + self._mirror.setEnabled(self._source.isEnabled()) + self._mirror.setChecked(self._source.isChecked()) diff --git a/negpy/translations/USER_GUIDE.en.md b/negpy/translations/USER_GUIDE.en.md index f5b6b6cfd..b57a974b6 100644 --- a/negpy/translations/USER_GUIDE.en.md +++ b/negpy/translations/USER_GUIDE.en.md @@ -223,7 +223,7 @@ Two cards: **Crop** sets the part of the picture you keep, and **Straighten** tu Choose the part of the picture to keep, by hand or with Auto Crop. * **Draw**: drag a crop on the picture. Drag a corner to resize it, the inside to move it, or outside the box to rotate. `Enter` or **Done** keeps the crop, and `Esc` puts back the crop you had before. The **×** removes a crop you drew. Drawing turns Auto Crop off. -* **Auto Crop**: finds the edges of the picture and crops to them. +* **Auto Crop** (also on the Geometry tab's bar): finds the edges of the picture and crops to them. * **Ratio**: the crop shape, such as `3:2` or `1:1`, turned to fit the frame. **Free** uses the shape NegPy finds, and you can drag one side at a time. **Detect** picks the nearest standard shape. * **Guide**: composition lines, such as thirds, while you draw. The rotate button turns them. * **Mode** (Auto Crop Settings fold): **Image only** crops to the picture. **Film edge** keeps the whole film with its border and holes. @@ -240,7 +240,7 @@ Turn the picture and make it level. * **Rotate** and **Flip**: turn the picture a quarter turn, or mirror it. The toolbar has the same buttons. * **Level**: draw a line along the horizon or a vertical edge, and the picture turns to make it level. -* **Square to Film**: lines the picture up with the edges of the film. It ignores what is in the picture, so a photo taken at a tilt stays tilted. +* **Square to Film** (also on the Geometry tab's bar): lines the picture up with the edges of the film. It ignores what is in the picture, so a photo taken at a tilt stays tilted. * **Fine Rotation**: turns the picture by small amounts. Positive is clockwise. * **Tilt** and **Swing** (fold): straighten a building shot from below (Tilt) or a wall shot from one side (Swing). **Draw Lines** sets both from lines along the four film edges. * **Crop Empty Corners**: crops away the empty corners that rotation, Tilt and Swing leave. @@ -271,7 +271,7 @@ How light or dark the print is, and how much contrast it has. * **Print Density**: how light or dark the print is (the darkroom's exposure time). Lower is lighter. * **ISO-R Grade**: contrast (the darkroom's paper grade). A lower number gives more contrast; about 110 is normal. -* **Auto**: **Auto Density** and **Auto Grade** set brightness and contrast for each frame, so a roll prints evenly. A slider then shows the automatic value: move it to adjust, double-click it to go back. The Auto menu also has: +* **Auto** (the wand, also on the Exposure tab's bar): **Auto Density** and **Auto Grade** set brightness and contrast for each frame, so a roll prints evenly. A slider then shows the automatic value: move it to adjust, double-click it to go back. The Auto menu also has: * **Set Targets…**: the brightness and contrast Auto aims for. * **Bake Auto Density/Grade**: keeps this frame's automatic values and turns Auto off for it. * **Hold Black**: when you lighten the shadows, the deepest blacks stay black. @@ -397,8 +397,8 @@ On a lith print only Selenium and Gold work. On a cyanotype no toner works. Removes dust, hairs and scratches. Each repair is rebuilt from the clean film around it. * **Overlay**: **Marked** shows what dust removal found, **IR** the scan's infrared channel. -* **Optical Removal**: finds dust and hairs by itself. **Spot Threshold** and **Hair Threshold**: lower finds more but can remove real detail; 1 is off. **Size**: the largest dust to remove. To protect an area, right-drag over it or right-click a mark; the cursor button skips the right-click menu. -* **IR Removal** (scans with an infrared channel): finds dust in infrared, where the picture does not show. **IR Threshold**: lower finds more. B&W and Kodachrome are skipped: their picture shows in infrared too. The channel comes from 4-channel TIFF and DNG scans or an `_IR.tif` file beside the scan. +* **Optical Removal** (also on the Finish tab's bar): finds dust and hairs by itself. **Spot Threshold** and **Hair Threshold**: lower finds more but can remove real detail; 1 is off. **Size**: the largest dust to remove. To protect an area, right-drag over it or right-click a mark; the cursor button skips the right-click menu. +* **IR Removal** (scans with an infrared channel, also on the Finish tab's bar): finds dust in infrared, where the picture does not show. **IR Threshold**: lower finds more. B&W and Kodachrome are skipped: their picture shows in infrared too. The channel comes from 4-channel TIFF and DNG scans or an `_IR.tif` file beside the scan. * **Heal**: click or paint over dust. Only marks that stand out are repaired, so paint generously. * **Scratch**: click points along a scratch or hair, then press Enter. * **Line**: click once on a long straight scratch, and NegPy finds and repairs the whole line. **Line Sensitivity**: lower finds fainter lines. diff --git a/negpy/translations/USER_GUIDE.ja.md b/negpy/translations/USER_GUIDE.ja.md index 9b20d3cec..fbd388623 100644 --- a/negpy/translations/USER_GUIDE.ja.md +++ b/negpy/translations/USER_GUIDE.ja.md @@ -94,13 +94,13 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室 - **修復**:ほこり除去で書き換えた割合。 - **色域**:プルーフのプリンターで印刷できない割合(プルーフ中のみ)。 - + ## トリミング 写真のどの部分を残すかを、手で、または自動トリミングで選びます。 - **描く**:写真の上でトリミングをドラッグします。角をドラッグでサイズ変更、内側で移動、枠の外側で回転します。`Enter` または**完了**でトリミングを残し、`Esc` で前のトリミングに戻します。**×** で描いたトリミングを消去します。描くと自動トリミングはオフになります。 -- **自動トリミング**:写真の端を見つけてトリミングします。 +- **自動トリミング**(ジオメトリタブのバーにもあります):写真の端を見つけてトリミングします。 - **比率**:`3:2` や `1:1` などのトリミングの形。フレームに合わせて向きを変えます。**フリー**は NegPy が見つけた形を使い、一辺ずつドラッグできます。**検出**は最も近い標準の形を選びます。 - **ガイド**:描くときの三分割などの構図線。回転ボタンで向きを変えます。 - **モード**(自動トリミングの設定の折りたたみ):**画像のみ**は写真にトリミングします。**フィルムエッジ**は縁と穴を含むフィルム全体を残します。 @@ -110,14 +110,14 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室 自動トリミングには、スキャナーの光やホルダーがスキャンの中で最も明るいことが必要です。写真が光と同じくらい明るいときや、隣のフレームが一辺の大部分を占めるときは、そのフレームをトリミングしません。手でトリミングするか、**フィルムエッジ**を使って内側に切り詰めてください。 - + ## まっすぐにする 写真を回転し、水平にします。 - **回転**と**反転**:写真を 90° 回転、または鏡のように反転します。ツールバーにも同じボタンがあります。 - **水平にする**:地平線か垂直な線に沿って線を引くと、写真が回転して水平になります。 -- **フィルムに合わせる**:写真をフィルムの端に合わせます。写っているものは見ないため、傾けて撮った写真は傾いたままです。 +- **フィルムに合わせる**(ジオメトリタブのバーにもあります):写真をフィルムの端に合わせます。写っているものは見ないため、傾けて撮った写真は傾いたままです。 - **微細回転**:写真をわずかに回転します。正の値で時計回りです。 - **ティルト**と**スイング**(折りたたみ):下から撮った建物(ティルト)や横から撮った壁(スイング)をまっすぐにします。**線を描く**はフィルムの 4 辺に沿った線から両方を設定します。 - **空の角をトリミング**:回転、ティルト、スイングでできる空の角を切り取ります。 @@ -135,14 +135,14 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室 - **リングアラウンド**(`Shift+F`):少しずつ色を変えた写真をグリッドで表示します。正しく見えるものをクリックします。`Esc` で閉じます。 - **色かぶり除去**:写真の暗い部分と明るい部分で異なる色かぶりを取り除きます。カラーネガでは 1(最大)、スライドでは 0 から始まります。葉、空、水はグレーとみなさないため、色はそのまま残ります。 - + ## トーン:明るさとコントラスト プリントの明るさと、コントラストの強さです。 - **プリント濃度**:プリントの明るさ(暗室での露光時間)。低いほど明るくなります。 - **ISO-R グレード**:コントラスト(暗室での印画紙の号数)。数値が低いほどコントラストが強くなり、約 110 が標準です。 -- **自動**:**自動濃度**と**自動グレード**がフレームごとに明るさとコントラストを設定し、ロール全体がそろって仕上がります。スライダーには自動の値が表示されます。動かすとそこから調整し、ダブルクリックで戻ります。自動メニューには次の項目もあります: +- **自動**(魔法の杖。露光タブのバーにもあります):**自動濃度**と**自動グレード**がフレームごとに明るさとコントラストを設定し、ロール全体がそろって仕上がります。スライダーには自動の値が表示されます。動かすとそこから調整し、ダブルクリックで戻ります。自動メニューには次の項目もあります: - **目標を設定…**:自動が目指す明るさとコントラスト。 - **自動濃度/グレードを固定**:このフレームの自動値を保存し、このフレームの自動をオフにします。 - **黒点を維持**:シャドウを明るくしても、最も深い黒は黒のままです。 @@ -252,14 +252,14 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室 **スプリットトーン**(すべてのプリント):**シャドウの強さ**と**ハイライトの強さ**でシャドウとハイライトに色を加えます。**シャドウの色相**と**ハイライトの色相**で色を選びます。これらは強さが 0 より大きいときに表示されます。 - + ## レタッチ:ほこり、毛、傷 ほこり、毛、傷を取り除きます。修復した部分は周りのきれいなフィルムから作り直します。 - **オーバーレイ**:**検出箇所**はほこり除去で見つかった場所、**IR** はスキャンの赤外線チャンネルを表示します。 -- **光学的除去**:ほこりと毛を自動で見つけます。**スポットしきい値**と**毛のしきい値**:低いほど多く見つかりますが、本来の細部まで消えることがあります。1 でオフです。**サイズ**:取り除くほこりの最大サイズ。部分を保護するには、その上を右ドラッグするか、跡を右クリックします。カーソルボタンで右クリックメニューを省きます。 -- **IR除去**(赤外線チャンネルのあるスキャン):写真が写らない赤外線でほこりを見つけます。**赤外線しきい値**:低いほど多く見つかります。白黒とコダクロームは赤外線にも写真が写るため対象外です。チャンネルは 4 チャンネルの TIFF と DNG スキャン、またはスキャンの横の `_IR.tif` ファイルから読み込みます。 +- **光学的除去**(仕上げタブのバーにもあります):ほこりと毛を自動で見つけます。**スポットしきい値**と**毛のしきい値**:低いほど多く見つかりますが、本来の細部まで消えることがあります。1 でオフです。**サイズ**:取り除くほこりの最大サイズ。部分を保護するには、その上を右ドラッグするか、跡を右クリックします。カーソルボタンで右クリックメニューを省きます。 +- **IR除去**(赤外線チャンネルのあるスキャン。仕上げタブのバーにもあります):写真が写らない赤外線でほこりを見つけます。**赤外線しきい値**:低いほど多く見つかります。白黒とコダクロームは赤外線にも写真が写るため対象外です。チャンネルは 4 チャンネルの TIFF と DNG スキャン、またはスキャンの横の `_IR.tif` ファイルから読み込みます。 - **修復**:ほこりをクリックするか塗ります。目立つ跡だけを修復するため、広めに塗ってかまいません。 - **スクラッチ**:傷や毛に沿って点をクリックし、Enter を押します。 - **ライン**:長くまっすぐな傷を一度クリックすると、NegPy が線全体を見つけて修復します。**ライン感度**:低いほど薄い線も見つけます。 diff --git a/negpy/translations/USER_GUIDE.ko.md b/negpy/translations/USER_GUIDE.ko.md index 5383fc4c3..a59d79eb1 100644 --- a/negpy/translations/USER_GUIDE.ko.md +++ b/negpy/translations/USER_GUIDE.ko.md @@ -94,13 +94,13 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입 - **복구**: 먼지 제거가 다시 쓴 비율입니다. - **색역**: 교정 중에 교정용 프린터가 인쇄할 수 없는 비율입니다. - + ## 자르기 사진에서 남길 부분을 직접, 또는 자동 자르기로 고릅니다. - **그리기**: 사진 위에서 자르기 영역을 드래그합니다. 모서리를 드래그하면 크기를 바꾸고, 안쪽은 이동, 상자 바깥은 회전합니다. `Enter` 또는 **완료**는 자르기를 유지하고, `Esc`는 이전 자르기로 되돌립니다. **×**는 직접 그린 자르기를 지웁니다. 그리면 자동 자르기가 꺼집니다. -- **자동 자르기**: 사진의 가장자리를 찾아 자릅니다. +- **자동 자르기**(기하 탭 막대에도 있음): 사진의 가장자리를 찾아 자릅니다. - **비율**: `3:2`나 `1:1` 같은 자르기 모양으로, 프레임에 맞게 방향을 돌립니다. **자유**는 NegPy가 찾은 모양을 쓰고, 한 변씩 드래그할 수 있습니다. **감지**는 가장 가까운 표준 모양을 고릅니다. - **가이드**: 그리는 동안 보이는 삼분할 같은 구도선입니다. 회전 버튼으로 방향을 바꿉니다. - **모드**(자동 자르기 설정 접기): **이미지만**은 사진만 자릅니다. **필름 가장자리**는 테두리와 구멍을 포함한 필름 전체를 남깁니다. @@ -110,14 +110,14 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입 자동 자르기는 스캐너 광원이나 홀더가 스캔에서 가장 밝아야 합니다. 사진이 광원만큼 밝거나 옆 프레임이 한쪽 변의 큰 부분을 차지하면 그 프레임은 자르지 않습니다. 직접 자르거나 **필름 가장자리**를 쓰고 안쪽으로 다듬으세요. - + ## 바로잡기 사진을 돌리고 수평을 맞춥니다. - **회전**과 **뒤집기**: 사진을 90° 돌리거나 거울처럼 뒤집습니다. 도구 막대에도 같은 버튼이 있습니다. - **수평 맞추기**: 수평선이나 수직 가장자리를 따라 선을 그리면 사진이 회전해 수평을 맞춥니다. -- **필름에 맞추기**: 사진을 필름 가장자리에 맞춥니다. 사진의 내용은 보지 않으므로 기울여 찍은 사진은 기울어진 채로 남습니다. +- **필름에 맞추기**(기하 탭 막대에도 있음): 사진을 필름 가장자리에 맞춥니다. 사진의 내용은 보지 않으므로 기울여 찍은 사진은 기울어진 채로 남습니다. - **미세 회전**: 사진을 조금씩 회전합니다. 양수는 시계 방향입니다. - **틸트**와 **스윙**(접기): 아래에서 찍은 건물(틸트)이나 옆에서 찍은 벽(스윙)을 바로잡습니다. **선 그리기**는 필름의 네 변을 따라 그린 선으로 둘 다 설정합니다. - **빈 모서리 자르기**: 회전, 틸트, 스윙으로 생긴 빈 모서리를 잘라냅니다. @@ -135,14 +135,14 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입 - **링 어라운드**(`Shift+F`): 색을 조금씩 바꾼 사진을 격자로 보여줍니다. 알맞아 보이는 것을 클릭하세요. `Esc`로 닫습니다. - **색 편향 제거**: 사진의 어두운 부분과 밝은 부분에서 서로 다른 색 편향을 없앱니다. 컬러 네거티브에서는 1(최대), 슬라이드에서는 0에서 시작합니다. 잎, 하늘, 물은 회색으로 보지 않으므로 색이 그대로 남습니다. - + ## 톤: 밝기와 대비 인화의 밝기와 대비의 세기입니다. - **인화 농도**: 인화의 밝기(암실의 노출 시간)입니다. 낮을수록 밝아집니다. - **ISO-R 등급**: 대비(암실의 인화지 호수)입니다. 숫자가 낮을수록 대비가 강해지며, 약 110이 보통입니다. -- **자동**: **자동 농도**와 **자동 등급**이 프레임마다 밝기와 대비를 정해 롤 전체가 고르게 인화됩니다. 슬라이더에는 자동 값이 표시됩니다. 움직이면 그 값에서 조정하고, 더블클릭하면 되돌아갑니다. 자동 메뉴에는 다음 항목도 있습니다: +- **자동**(마법 지팡이, 노출 탭 막대에도 있음): **자동 농도**와 **자동 등급**이 프레임마다 밝기와 대비를 정해 롤 전체가 고르게 인화됩니다. 슬라이더에는 자동 값이 표시됩니다. 움직이면 그 값에서 조정하고, 더블클릭하면 되돌아갑니다. 자동 메뉴에는 다음 항목도 있습니다: - **목표 설정…**: 자동이 목표로 하는 밝기와 대비입니다. - **자동 농도/등급 확정**: 이 프레임의 자동 값을 저장하고 이 프레임의 자동을 끕니다. - **블랙 포인트 유지**: 어두운 영역을 밝게 해도 가장 깊은 검정은 그대로 유지됩니다. @@ -252,14 +252,14 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입 **스플릿 토닝**(모든 인화): **어두운 영역 강도**와 **밝은 영역 강도**로 어두운 영역과 밝은 영역에 색을 더합니다. **어두운 영역 색조**와 **밝은 영역 색조**로 색을 고르며, 강도가 0보다 클 때 표시됩니다. - + ## 리터치: 먼지, 머리카락, 스크래치 먼지, 머리카락, 스크래치를 없앱니다. 복구한 부분은 주변의 깨끗한 필름으로 다시 만듭니다. - **오버레이**: **표시됨**은 먼지 제거가 찾은 곳을, **IR**은 스캔의 적외선 채널을 보여줍니다. -- **광학 제거**: 먼지와 머리카락을 자동으로 찾습니다. **스팟 임계값**과 **헤어 임계값**: 낮을수록 더 많이 찾지만 실제 세부도 지울 수 있으며, 1은 꺼짐입니다. **크기**: 제거할 먼지의 최대 크기입니다. 어떤 영역을 보호하려면 그 위를 오른쪽 드래그하거나 자국을 오른쪽 클릭하세요. 커서 버튼을 켜면 오른쪽 클릭 메뉴를 건너뜁니다. -- **IR 제거**(적외선 채널이 있는 스캔): 사진이 보이지 않는 적외선에서 먼지를 찾습니다. **IR 임계값**: 낮을수록 더 많이 찾습니다. 흑백과 코다크롬은 적외선에서도 사진이 보이므로 건너뜁니다. 채널은 4채널 TIFF·DNG 스캔이나 스캔 옆의 `_IR.tif` 파일에서 읽습니다. +- **광학 제거**(마무리 탭 막대에도 있음): 먼지와 머리카락을 자동으로 찾습니다. **스팟 임계값**과 **헤어 임계값**: 낮을수록 더 많이 찾지만 실제 세부도 지울 수 있으며, 1은 꺼짐입니다. **크기**: 제거할 먼지의 최대 크기입니다. 어떤 영역을 보호하려면 그 위를 오른쪽 드래그하거나 자국을 오른쪽 클릭하세요. 커서 버튼을 켜면 오른쪽 클릭 메뉴를 건너뜁니다. +- **IR 제거**(적외선 채널이 있는 스캔, 마무리 탭 막대에도 있음): 사진이 보이지 않는 적외선에서 먼지를 찾습니다. **IR 임계값**: 낮을수록 더 많이 찾습니다. 흑백과 코다크롬은 적외선에서도 사진이 보이므로 건너뜁니다. 채널은 4채널 TIFF·DNG 스캔이나 스캔 옆의 `_IR.tif` 파일에서 읽습니다. - **복구**: 먼지를 클릭하거나 칠합니다. 눈에 띄는 자국만 복구하므로 넉넉하게 칠해도 됩니다. - **스크래치**: 스크래치나 머리카락을 따라 점을 클릭한 뒤 Enter를 누르세요. - **라인**: 길고 곧은 스크래치를 한 번 클릭하면 NegPy가 선 전체를 찾아 복구합니다. **라인 감도**: 낮을수록 희미한 선도 찾습니다. diff --git a/negpy/translations/USER_GUIDE.zh_CN.md b/negpy/translations/USER_GUIDE.zh_CN.md index 7bd291831..1ca747eba 100644 --- a/negpy/translations/USER_GUIDE.zh_CN.md +++ b/negpy/translations/USER_GUIDE.zh_CN.md @@ -94,13 +94,13 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样 - **修复**:除尘改写的比例。 - **色域**:软打样时校样打印机无法印出的比例。 - + ## 裁切 手动或用自动裁切,选择要保留的画面部分。 - **绘制**:在画面上拖出裁切范围。拖动角点调整大小,拖动内部移动,在框外拖动旋转。`Enter` 或**完成**保留裁切,`Esc` 恢复之前的裁切。**×** 清除你绘制的裁切。绘制会关闭自动裁切。 -- **自动裁切**:找到画面边缘并按其裁切。 +- **自动裁切**(几何标签栏上也有):找到画面边缘并按其裁切。 - **比例**:裁切形状,如 `3:2` 或 `1:1`,会按画面转向。**自由**使用 NegPy 找到的形状,并可单独拖动一条边。**检测**选择最接近的标准形状。 - **参考线**:绘制时显示的三分法等构图线。旋转按钮可改变方向。 - **模式**(自动裁切设置折叠区):**仅图像**只裁到画面。**胶片边缘**保留带边框和齿孔的整幅胶片。 @@ -110,14 +110,14 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样 自动裁切要求扫描仪光源或片夹是扫描中最亮的部分。当画面和光源一样亮,或相邻帧占据一边的大部分时,它不会裁切该帧。请手动裁切,或使用**胶片边缘**再向内修剪。 - + ## 拉直 旋转画面并把它摆正。 - **旋转**和**翻转**:把画面转 90°,或镜像翻转。工具栏上也有相同的按钮。 - **水平校正**:沿地平线或竖直边缘画一条线,画面随之旋转摆正。 -- **对齐胶片**:让画面与胶片边缘对齐。它不看画面内容,所以有意斜拍的照片仍保持倾斜。 +- **对齐胶片**(几何标签栏上也有):让画面与胶片边缘对齐。它不看画面内容,所以有意斜拍的照片仍保持倾斜。 - **精细旋转**:小幅旋转画面。正值为顺时针。 - **俯仰**和**摆动**(折叠区):校正从下往上拍的建筑(俯仰)或从侧面拍的墙(摆动)。**绘制参考线**根据沿胶片四边画的线同时设置两者。 - **裁掉空白角**:裁掉旋转、俯仰和摆动留下的空白角。 @@ -135,14 +135,14 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样 - **环绕试样**(`Shift+F`):以网格显示颜色略有不同的多个版本。点击看起来正确的那一格。`Esc` 关闭。 - **去色偏**:去除画面暗部与亮部不同的色偏。彩色负片从 1(完全)开始,反转片从 0 开始。树叶、天空和水不算作灰色,因此保持原有颜色。 - + ## 色调:亮度与反差 照片的明暗程度和反差大小。 - **印相密度**:照片的明暗(暗房中的曝光时间)。数值越低越亮。 - **ISO-R 反差等级**:反差(暗房中的相纸号数)。数值越低反差越大,约 110 为常规。 -- **自动**:**自动密度**和**自动反差**为每帧设置亮度和反差,让整卷印出来一致。滑块显示自动值:拖动即在此基础上调整,双击则恢复。自动菜单还包含: +- **自动**(魔杖图标,曝光标签栏上也有):**自动密度**和**自动反差**为每帧设置亮度和反差,让整卷印出来一致。滑块显示自动值:拖动即在此基础上调整,双击则恢复。自动菜单还包含: - **设置目标…**:自动功能追求的亮度和反差。 - **固化自动密度/反差**:保存此帧的自动值,并关闭此帧的自动功能。 - **保持黑点**:提亮暗部时,最深的黑仍保持黑色。 @@ -252,14 +252,14 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样 **分离色调**(所有照片):**暗部强度**和**高光强度**为暗部和高光加色。**暗部色相**和**高光色相**选择颜色,在强度大于 0 时显示。 - + ## 修复:灰尘、毛发、划痕 去除灰尘、毛发和划痕。每处修复都用周围干净的胶片重建。 - **叠加显示**:**已标记**显示除尘找到的位置,**红外**显示扫描的红外通道。 -- **光学除尘**:自动查找灰尘和毛发。**尘点阈值**和**毛发阈值**:数值越低检出越多,但可能误删真实细节;1 为关闭。**尺寸**:要去除的最大灰尘。要保护某区域,在其上右键拖动,或右键点击某个痕迹;光标按钮可跳过右键菜单。 -- **红外除尘**(带红外通道的扫描):在画面不可见的红外中查找灰尘。**红外阈值**:数值越低检出越多。黑白和柯达克罗姆胶片会被跳过,因为它们的画面在红外中也可见。红外通道来自 4 通道 TIFF 和 DNG 扫描,或扫描旁的 `_IR.tif` 文件。 +- **光学除尘**(修饰标签栏上也有):自动查找灰尘和毛发。**尘点阈值**和**毛发阈值**:数值越低检出越多,但可能误删真实细节;1 为关闭。**尺寸**:要去除的最大灰尘。要保护某区域,在其上右键拖动,或右键点击某个痕迹;光标按钮可跳过右键菜单。 +- **红外除尘**(带红外通道的扫描,修饰标签栏上也有):在画面不可见的红外中查找灰尘。**红外阈值**:数值越低检出越多。黑白和柯达克罗姆胶片会被跳过,因为它们的画面在红外中也可见。红外通道来自 4 通道 TIFF 和 DNG 扫描,或扫描旁的 `_IR.tif` 文件。 - **修复**:点击或涂抹灰尘。只修复明显的痕迹,所以可以涂得宽一些。 - **划痕**:沿划痕或毛发点击各点,然后按 Enter。 - **直线**:在一条长直划痕上点击一次,NegPy 会找到并修复整条线。**直线灵敏度**:数值越低能找到越淡的线。 diff --git a/tests/test_tab_header.py b/tests/test_tab_header.py index b31ab7a1b..a3ed22e54 100644 --- a/tests/test_tab_header.py +++ b/tests/test_tab_header.py @@ -4,11 +4,13 @@ from unittest.mock import MagicMock, patch from PyQt6.QtGui import QIcon +from PyQt6.QtWidgets import QPushButton, QWidget from negpy.desktop.settings_catalog import COLOR_FIELDS, GEOMETRY_FIELDS, rows_for_fields, rows_for_section from negpy.desktop.view.sidebar.controls_panel import ControlsPanel from negpy.desktop.view.widgets.collapsible import CollapsibleSection from negpy.desktop.view.widgets.tab_header import TabHeader +from negpy.desktop.view.widgets.segmented_choice import ToggleMenuButton def _card(count: int, hidden: bool = False) -> CollapsibleSection: @@ -233,3 +235,61 @@ def test_pick_better_leads_the_exposure_header(): assert header._header_row.indexOf(panel.pick_better_btn) == 0 panel.controller.toggle_pick_better.assert_called_once_with(force=True) + + +def test_auto_mirror_shares_the_tone_cards_menu_beside_pick_better(): + header = TabHeader("Exposure") + panel = MagicMock() + panel._pick_better_tooltip = ControlsPanel._pick_better_tooltip + auto = ToggleMenuButton("fa5s.magic", "", "Auto") + density = auto.add_toggle("Auto Density", "") + panel.tone_sidebar.auto_btn = auto + + ControlsPanel._add_pick_better(panel, header) + ControlsPanel._add_auto_mirror(panel, header) + + assert header._header_row.indexOf(panel.auto_mirror_btn) == 1 + assert panel.auto_mirror_btn.menu() is auto.choice_menu + density.setChecked(True) + assert panel.auto_mirror_btn.isChecked() + density.setChecked(False) + assert not panel.auto_mirror_btn.isChecked() + + +def test_mirror_forwards_clicks_and_follows_its_source(): + header = TabHeader("Finish") + card = QWidget() + source = QPushButton(card) + source.setCheckable(True) + clicks = [] + source.clicked.connect(clicks.append) + + mirror = header.add_mirror(source) + mirror.click() + assert clicks == [True] + assert mirror.isChecked() + + source.setChecked(False) + assert not mirror.isChecked() + + # A collapsed card is never shown, yet its explicit hide still reaches the mirror. + source.setVisible(False) + assert mirror.isHidden() + source.setVisible(True) + assert not mirror.isHidden() + + card.setEnabled(False) + assert not mirror.isEnabled() + + +def test_mirror_of_a_one_shot_action_stays_unchecked(): + header = TabHeader("Geometry") + source = QPushButton() + clicks = [] + source.clicked.connect(clicks.append) + + mirror = header.add_mirror(source) + mirror.click() + + assert clicks == [False] + assert not mirror.isCheckable()