shinestacker 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (133) hide show
  1. {shinestacker-0.4.0 → shinestacker-0.5.0}/CHANGELOG.md +12 -0
  2. {shinestacker-0.4.0/src/shinestacker.egg-info → shinestacker-0.5.0}/PKG-INFO +1 -1
  3. shinestacker-0.5.0/scripts/git-rev-list.sh +6 -0
  4. shinestacker-0.5.0/src/shinestacker/_version.py +1 -0
  5. shinestacker-0.5.0/src/shinestacker/app/about_dialog.py +96 -0
  6. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/config/gui_constants.py +7 -5
  7. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/actions_window.py +8 -0
  8. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/main_window.py +13 -8
  9. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/brush_tool.py +23 -6
  10. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/display_manager.py +57 -20
  11. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/image_editor.py +5 -9
  12. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/image_editor_ui.py +53 -15
  13. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/io_gui_handler.py +71 -22
  14. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/io_manager.py +23 -7
  15. {shinestacker-0.4.0 → shinestacker-0.5.0/src/shinestacker.egg-info}/PKG-INFO +1 -1
  16. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker.egg-info/SOURCES.txt +1 -0
  17. shinestacker-0.4.0/src/shinestacker/_version.py +0 -1
  18. shinestacker-0.4.0/src/shinestacker/app/about_dialog.py +0 -28
  19. {shinestacker-0.4.0 → shinestacker-0.5.0}/.coveragerc +0 -0
  20. {shinestacker-0.4.0 → shinestacker-0.5.0}/.flake8 +0 -0
  21. {shinestacker-0.4.0 → shinestacker-0.5.0}/.github/workflows/ci-multiplatform.yml +0 -0
  22. {shinestacker-0.4.0 → shinestacker-0.5.0}/.github/workflows/pylint.yml +0 -0
  23. {shinestacker-0.4.0 → shinestacker-0.5.0}/.github/workflows/pypi-publish.yml +0 -0
  24. {shinestacker-0.4.0 → shinestacker-0.5.0}/.github/workflows/release.yml +0 -0
  25. {shinestacker-0.4.0 → shinestacker-0.5.0}/.gitignore +0 -0
  26. {shinestacker-0.4.0 → shinestacker-0.5.0}/.pylintrc +0 -0
  27. {shinestacker-0.4.0 → shinestacker-0.5.0}/.readthedocs.yaml +0 -0
  28. {shinestacker-0.4.0 → shinestacker-0.5.0}/LICENSE +0 -0
  29. {shinestacker-0.4.0 → shinestacker-0.5.0}/MANIFEST.in +0 -0
  30. {shinestacker-0.4.0 → shinestacker-0.5.0}/README.md +0 -0
  31. {shinestacker-0.4.0 → shinestacker-0.5.0}/THIRD_PARTY_LICENSES.txt +0 -0
  32. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/alignment.md +0 -0
  33. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/api.md +0 -0
  34. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/balancing.md +0 -0
  35. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/conf.py +0 -0
  36. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/focus_stacking.md +0 -0
  37. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/gui.md +0 -0
  38. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/index.md +0 -0
  39. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/job.md +0 -0
  40. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/main.md +0 -0
  41. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/multilayer.md +0 -0
  42. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/noise.md +0 -0
  43. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/requirements.txt +0 -0
  44. {shinestacker-0.4.0 → shinestacker-0.5.0}/docs/vignetting.md +0 -0
  45. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/coffee.gif +0 -0
  46. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/coffee_stack.jpg +0 -0
  47. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/extreme-vignetting.jpg +0 -0
  48. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/flies.gif +0 -0
  49. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/flies_stack.jpg +0 -0
  50. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/flow-diagram.png +0 -0
  51. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/gui-finder.png +0 -0
  52. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/gui-project-new.png +0 -0
  53. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/gui-project-run.png +0 -0
  54. {shinestacker-0.4.0 → shinestacker-0.5.0}/img/gui-retouch.png +0 -0
  55. {shinestacker-0.4.0 → shinestacker-0.5.0}/pyproject.toml +0 -0
  56. {shinestacker-0.4.0 → shinestacker-0.5.0}/requirements.txt +0 -0
  57. {shinestacker-0.4.0 → shinestacker-0.5.0}/scripts/build_release.py +0 -0
  58. {shinestacker-0.4.0 → shinestacker-0.5.0}/scripts/validate-tomli.py +0 -0
  59. {shinestacker-0.4.0 → shinestacker-0.5.0}/setup.cfg +0 -0
  60. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/__init__.py +0 -0
  61. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/__init__.py +0 -0
  62. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/align.py +0 -0
  63. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/balance.py +0 -0
  64. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  65. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/denoise.py +0 -0
  66. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/depth_map.py +0 -0
  67. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/exif.py +0 -0
  68. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/multilayer.py +0 -0
  69. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/noise_detection.py +0 -0
  70. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/pyramid.py +0 -0
  71. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  72. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/stack.py +0 -0
  73. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/stack_framework.py +0 -0
  74. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/utils.py +0 -0
  75. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/vignetting.py +0 -0
  76. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  77. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/app/__init__.py +0 -0
  78. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/app/app_config.py +0 -0
  79. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/app/gui_utils.py +0 -0
  80. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/app/help_menu.py +0 -0
  81. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/app/main.py +0 -0
  82. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/app/open_frames.py +0 -0
  83. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/app/project.py +0 -0
  84. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/app/retouch.py +0 -0
  85. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/config/__init__.py +0 -0
  86. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/config/config.py +0 -0
  87. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/config/constants.py +0 -0
  88. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/core/__init__.py +0 -0
  89. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/core/colors.py +0 -0
  90. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/core/core_utils.py +0 -0
  91. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/core/exceptions.py +0 -0
  92. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/core/framework.py +0 -0
  93. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/core/logging.py +0 -0
  94. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/__init__.py +0 -0
  95. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/action_config.py +0 -0
  96. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/colors.py +0 -0
  97. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/gui_images.py +0 -0
  98. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/gui_logging.py +0 -0
  99. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/gui_run.py +0 -0
  100. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  101. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  102. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  103. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  104. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/img/close-round-line-icon.png +0 -0
  105. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/img/forward-button-icon.png +0 -0
  106. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/img/play-button-round-icon.png +0 -0
  107. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/img/plus-round-line-icon.png +0 -0
  108. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/new_project.py +0 -0
  109. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/project_converter.py +0 -0
  110. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/project_editor.py +0 -0
  111. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/project_model.py +0 -0
  112. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/gui/select_path_widget.py +0 -0
  113. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/__init__.py +0 -0
  114. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/base_filter.py +0 -0
  115. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/brush.py +0 -0
  116. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  117. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/brush_preview.py +0 -0
  118. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  119. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/exif_data.py +0 -0
  120. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/file_loader.py +0 -0
  121. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  122. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/icon_container.py +0 -0
  123. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/image_filters.py +0 -0
  124. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/image_viewer.py +0 -0
  125. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/layer_collection.py +0 -0
  126. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  127. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/undo_manager.py +0 -0
  128. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  129. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  130. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  131. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  132. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker.egg-info/requires.txt +0 -0
  133. {shinestacker-0.4.0 → shinestacker-0.5.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [v0.5.0] - 2025-08-20
6
+ **GUI and robustness improvements**
7
+
8
+ ### Changes
9
+
10
+ * layer selection highlightted with a blue border
11
+ * improved font rendering in brush preview
12
+ * fixed thumbnail spacing
13
+ * fixed and improved save strategy for retouched images
14
+ * added checks for updated version in about dialog
15
+ * disable "Save" and "Save As..." menus if do not apply to current status
16
+
5
17
  ---
6
18
 
7
19
  ## [v0.4.0] - 2025-08-19
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ git rev-list --objects --all |
3
+ git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
4
+ awk '/^blob/ {print $3,$4}' |
5
+ sort -n -k1 |
6
+ tail -50
@@ -0,0 +1 @@
1
+ __version__ = '0.5.0'
@@ -0,0 +1,96 @@
1
+ # pylint: disable=C0114, C0116, E0611, W0718
2
+ import json
3
+ from urllib.request import urlopen, Request
4
+ from urllib.error import URLError
5
+ from PySide6.QtWidgets import QMessageBox
6
+ from PySide6.QtCore import Qt
7
+ from .. import __version__
8
+ from .. config.constants import constants
9
+
10
+
11
+ def compare_versions(current, latest):
12
+ def parse_version(v):
13
+ v = v.lstrip('v')
14
+ parts = v.split('.')
15
+ result = []
16
+ for part in parts:
17
+ try:
18
+ result.append(int(part))
19
+ except ValueError:
20
+ result.append(part)
21
+ return result
22
+ current_parts = parse_version(current)
23
+ latest_parts = parse_version(latest)
24
+ for i in range(max(len(current_parts), len(latest_parts))):
25
+ c = current_parts[i] if i < len(current_parts) else 0
26
+ l = latest_parts[i] if i < len(latest_parts) else 0 # noqa: E741
27
+ if isinstance(c, int) and isinstance(l, int):
28
+ if c < l:
29
+ return -1
30
+ if c > l:
31
+ return 1
32
+ else:
33
+ if str(c) < str(l):
34
+ return -1
35
+ if str(c) > str(l):
36
+ return 1
37
+ return 0
38
+
39
+
40
+ def get_latest_version():
41
+ try:
42
+ url = "https://api.github.com/repos/lucalista/shinestacker/releases/latest"
43
+ headers = {'User-Agent': 'ShineStacker'}
44
+ req = Request(url, headers=headers)
45
+ with urlopen(req, timeout=5) as response:
46
+ data = json.loads(response.read().decode())
47
+ return data['tag_name']
48
+ except (URLError, ValueError, KeyError, TimeoutError):
49
+ return None
50
+
51
+
52
+ def show_about_dialog():
53
+ version_clean = __version__.split("+", maxsplit=1)[0]
54
+ latest_version = None
55
+ try:
56
+ latest_version = get_latest_version()
57
+ except Exception:
58
+ pass
59
+ update_text = ""
60
+ # pyling: disable=XXX
61
+ if latest_version:
62
+ latest_clean = latest_version.lstrip('v')
63
+ if compare_versions(version_clean, latest_clean) < 0:
64
+ update_text = f"""
65
+ <p style="color: red; font-weight: bold;">
66
+ Update available! Latest version: {latest_version}
67
+ <br><a href="https://github.com/lucalista/shinestacker/releases/latest">Download here</a>
68
+ </p>
69
+ """ # noqa E501
70
+ else:
71
+ update_text = f"""
72
+ <p style="color: green; font-weight: bold;">
73
+ You are using the lastet version: {latest_version}.
74
+ </p>
75
+ """
76
+ about_text = f"""
77
+ <h3>{constants.APP_TITLE}</h3>
78
+ <h4>version: v{version_clean}</h4>
79
+ {update_text}
80
+ <p style='font-weight: normal;'>App and framework to combine multiple images
81
+ into a single focused image.</p>
82
+ <p>Author: Luca Lista<br/>
83
+ Email: <a href="mailto:luka.lista@gmail.com">luka.lista@gmail.com</a></p>
84
+ <ul>
85
+ <li><a href="https://shinestacker.wordpress.com/">Website on Wordpress</a></li>
86
+ <li><a href="https://github.com/lucalista/shinestacker">GitHub project repository</a></li>
87
+ </ul>
88
+ """
89
+ # pyling: enable=XXX
90
+ msg = QMessageBox()
91
+ msg.setWindowTitle(f"About {constants.APP_STRING}")
92
+ msg.setIcon(QMessageBox.Icon.Information)
93
+ msg.setTextFormat(Qt.TextFormat.RichText)
94
+ msg.setText(about_text)
95
+ msg.setIcon(QMessageBox.Icon.NoIcon)
96
+ msg.exec_()
@@ -36,9 +36,10 @@ class _GuiConstants:
36
36
 
37
37
  THUMB_WIDTH = 120 # px
38
38
  THUMB_HEIGHT = 80 # px
39
- IMG_WIDTH = 100 # px
40
- IMG_HEIGHT = 80 # px
41
- LABEL_HEIGHT = 20 # px
39
+ THUMB_HI_COLOR = '#0000FF'
40
+ THUMB_LO_COLOR = '#0000FF'
41
+ THUMB_MASTER_HI_COLOR = '#0000FF'
42
+ THUMB_MASTER_LO_COLOR = 'transparent'
42
43
 
43
44
  MAX_UNDO_STEPS = 50
44
45
 
@@ -46,8 +47,9 @@ class _GuiConstants:
46
47
 
47
48
  UI_SIZES = {
48
49
  'brush_preview': (100, 80),
49
- 'thumbnail': (IMG_WIDTH, IMG_HEIGHT),
50
- 'master_thumb': (THUMB_WIDTH, THUMB_HEIGHT)
50
+ 'thumbnail_width': 100,
51
+ 'master_thumb': (THUMB_WIDTH, THUMB_HEIGHT),
52
+ 'label_height': 20
51
53
  }
52
54
 
53
55
  DEFAULT_BRUSH_HARDNESS = 50
@@ -30,6 +30,7 @@ class ActionsWindow(ProjectEditor):
30
30
  def mark_as_modified(self):
31
31
  self._modified_project = True
32
32
  self.project_buffer.append(self.project.clone())
33
+ self.save_actions_set_enabled(True)
33
34
  self.update_title()
34
35
 
35
36
  def close_project(self):
@@ -40,6 +41,7 @@ class ActionsWindow(ProjectEditor):
40
41
  self.job_list.clear()
41
42
  self.action_list.clear()
42
43
  self._modified_project = False
44
+ self.save_actions_set_enabled(False)
43
45
 
44
46
  def new_project(self):
45
47
  if not self._check_unsaved_changes():
@@ -51,8 +53,10 @@ class ActionsWindow(ProjectEditor):
51
53
  self.job_list.clear()
52
54
  self.action_list.clear()
53
55
  self.set_project(Project())
56
+ self.save_actions_set_enabled(False)
54
57
  dialog = NewProjectDialog(self)
55
58
  if dialog.exec() == QDialog.Accepted:
59
+ self.save_actions_set_enabled(True)
56
60
  input_folder = dialog.get_input_folder().split('/')
57
61
  working_path = '/'.join(input_folder[:-1])
58
62
  input_path = input_folder[-1]
@@ -142,6 +146,7 @@ class ActionsWindow(ProjectEditor):
142
146
  if len(self.project.jobs) > 0:
143
147
  self.job_list.setCurrentRow(0)
144
148
  self.activateWindow()
149
+ self.save_actions_set_enabled(True)
145
150
  for job in self.project.jobs:
146
151
  if 'working_path' in job.params.keys():
147
152
  working_path = job.params['working_path']
@@ -256,3 +261,6 @@ class ActionsWindow(ProjectEditor):
256
261
  if dialog.exec() == QDialog.Accepted:
257
262
  self.on_job_selected(self.job_list.currentRow())
258
263
  self.mark_as_modified()
264
+
265
+ def save_actions_set_enabled(self, enabled):
266
+ pass
@@ -189,19 +189,24 @@ class MainWindow(ActionsWindow, LogManager):
189
189
  open_action.setShortcut("Ctrl+O")
190
190
  open_action.triggered.connect(self.open_project)
191
191
  menu.addAction(open_action)
192
- save_action = QAction("&Save", self)
193
- save_action.setShortcut("Ctrl+S")
194
- save_action.triggered.connect(self.save_project)
195
- menu.addAction(save_action)
196
- save_as_action = QAction("Save &As...", self)
197
- save_as_action.setShortcut("Ctrl+Shift+S")
198
- save_as_action.triggered.connect(self.save_project_as)
199
- menu.addAction(save_as_action)
192
+ self.save_action = QAction("&Save", self)
193
+ self.save_action.setShortcut("Ctrl+S")
194
+ self.save_action.triggered.connect(self.save_project)
195
+ menu.addAction(self.save_action)
196
+ self.save_as_action = QAction("Save &As...", self)
197
+ self.save_as_action.setShortcut("Ctrl+Shift+S")
198
+ self.save_as_action.triggered.connect(self.save_project_as)
199
+ menu.addAction(self.save_as_action)
200
+ self.save_actions_set_enabled(False)
200
201
  close_action = QAction("&Close", self)
201
202
  close_action.setShortcut("Ctrl+W")
202
203
  close_action.triggered.connect(self.close_project)
203
204
  menu.addAction(close_action)
204
205
 
206
+ def save_actions_set_enabled(self, enabled):
207
+ self.save_action.setEnabled(enabled)
208
+ self.save_as_action.setEnabled(enabled)
209
+
205
210
  def add_edit_menu(self, menubar):
206
211
  menu = menubar.addMenu("&Edit")
207
212
  undo_action = QAction("&Undo", self)
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0913, R0917, R0914
2
2
  import numpy as np
3
- from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
3
+ from PySide6.QtWidgets import QApplication, QLabel
4
+ from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QFont
4
5
  from PySide6.QtCore import Qt, QPoint
5
6
  from .brush_gradient import create_default_brush_gradient
6
7
  from .. config.gui_constants import gui_constants
@@ -18,11 +19,16 @@ class BrushTool:
18
19
  self.opacity_slider = None
19
20
  self.flow_slider = None
20
21
  self._brush_mask_cache = {}
22
+ self.brush_text = None
21
23
 
22
24
  def setup_ui(self, brush, brush_preview, image_viewer, size_slider, hardness_slider,
23
25
  opacity_slider, flow_slider):
24
26
  self.brush = brush
25
27
  self.brush_preview = brush_preview
28
+ self.brush_text = QLabel(brush_preview.parent())
29
+ self.brush_text.setStyleSheet("color: navy; background: transparent;")
30
+ self.brush_text.setAlignment(Qt.AlignLeft | Qt.AlignTop)
31
+ self.brush_text.raise_()
26
32
  self.image_viewer = image_viewer
27
33
  self.size_slider = size_slider
28
34
  self.hardness_slider = hardness_slider
@@ -86,7 +92,7 @@ class BrushTool:
86
92
  pixmap = QPixmap(width, height)
87
93
  pixmap.fill(Qt.transparent)
88
94
  painter = QPainter(pixmap)
89
- painter.setRenderHint(QPainter.Antialiasing)
95
+ painter.setRenderHint(QPainter.TextAntialiasing, True)
90
96
  preview_size = min(self.brush.size, width + 30, height + 30)
91
97
  center_x, center_y = width // 2, height // 2
92
98
  radius = preview_size // 2
@@ -109,10 +115,21 @@ class BrushTool:
109
115
  painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
110
116
  if self.image_viewer.cursor_style == 'preview':
111
117
  painter.setPen(QPen(QColor(0, 0, 160)))
112
- painter.drawText(0, 10, f"Size: {int(self.brush.size)}px")
113
- painter.drawText(0, 25, f"Hardness: {self.brush.hardness}%")
114
- painter.drawText(0, 40, f"Opacity: {self.brush.opacity}%")
115
- painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
118
+ font = QApplication.font()
119
+ painter.setFont(font)
120
+ font.setHintingPreference(QFont.PreferFullHinting)
121
+ painter.setFont(font)
122
+ self.brush_text.setText(
123
+ f"Size: {int(self.brush.size)}px\n"
124
+ f"Hardness: {self.brush.hardness}%\n"
125
+ f"Opacity: {self.brush.opacity}%\n"
126
+ f"Flow: {self.brush.flow}%"
127
+ )
128
+ self.brush_text.adjustSize()
129
+ self.brush_text.move(10, self.brush_preview.height() // 2 + 125)
130
+ self.brush_text.show()
131
+ else:
132
+ self.brush_text.hide()
116
133
  painter.end()
117
134
  self.brush_preview.setPixmap(pixmap)
118
135
  self.image_viewer.update_brush_cursor()
@@ -1,6 +1,7 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121, R0902
2
2
  import numpy as np
3
- from PySide6.QtWidgets import QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog
3
+ from PySide6.QtWidgets import (QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog,
4
+ QAbstractItemView)
4
5
  from PySide6.QtGui import QPixmap, QImage
5
6
  from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal
6
7
  from .. config.gui_constants import gui_constants
@@ -39,6 +40,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
39
40
  self.update_timer = QTimer()
40
41
  self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
41
42
  self.update_timer.timeout.connect(self.process_pending_updates)
43
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
42
44
 
43
45
  def process_pending_updates(self):
44
46
  if self.needs_update:
@@ -64,15 +66,15 @@ class DisplayManager(QObject, LayerCollectionHandler):
64
66
  self.display_master_layer()
65
67
 
66
68
  def create_thumbnail(self, layer):
67
- if layer.dtype == np.uint16:
68
- layer = (layer // 256).astype(np.uint8)
69
- height, width = layer.shape[:2]
70
- if layer.ndim == 3 and layer.shape[-1] == 3:
71
- qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
69
+ source_layer = (layer // 256).astype(np.uint8) if layer.dtype == np.uint16 else layer
70
+ height, width = source_layer.shape[:2]
71
+ if layer.ndim == 3 and source_layer.shape[-1] == 3:
72
+ qimg = QImage(source_layer.data, width, height, 3 * width, QImage.Format_RGB888)
72
73
  else:
73
- qimg = QImage(layer.data, width, height, width, QImage.Format_Grayscale8)
74
+ qimg = QImage(source_layer.data, width, height, width, QImage.Format_Grayscale8)
74
75
  return QPixmap.fromImage(
75
- qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
76
+ qimg.scaledToWidth(
77
+ gui_constants.UI_SIZES['thumbnail_width'], Qt.SmoothTransformation))
76
78
 
77
79
  def update_thumbnails(self):
78
80
  self.update_master_thumbnail()
@@ -103,17 +105,22 @@ class DisplayManager(QObject, LayerCollectionHandler):
103
105
  self.master_thumbnail_label.setPixmap(pixmap)
104
106
 
105
107
  def add_thumbnail_item(self, thumbnail, label, i, is_current):
106
- item_widget = QWidget()
107
- layout = QVBoxLayout(item_widget)
108
- layout.setContentsMargins(0, 0, 0, 0)
109
- layout.setSpacing(0)
110
-
108
+ container = QWidget()
109
+ container.setFixedWidth(gui_constants.UI_SIZES['thumbnail_width'] + 4)
110
+ container.setObjectName("thumbnailContainer")
111
+ container_layout = QVBoxLayout(container)
112
+ container_layout.setContentsMargins(2, 2, 2, 2)
113
+ container_layout.setSpacing(0)
114
+ content_widget = QWidget()
115
+ content_layout = QVBoxLayout(content_widget)
116
+ content_layout.setContentsMargins(0, 0, 0, 0)
117
+ content_layout.setSpacing(0)
111
118
  thumbnail_label = QLabel()
112
119
  thumbnail_label.setPixmap(thumbnail)
113
120
  thumbnail_label.setAlignment(Qt.AlignCenter)
114
- layout.addWidget(thumbnail_label)
115
-
121
+ content_layout.addWidget(thumbnail_label)
116
122
  label_widget = ClickableLabel(label)
123
+ label_widget.setFixedHeight(gui_constants.UI_SIZES['label_height'])
117
124
  label_widget.setAlignment(Qt.AlignCenter)
118
125
 
119
126
  def rename_label(label_widget, old_label, i):
@@ -124,21 +131,45 @@ class DisplayManager(QObject, LayerCollectionHandler):
124
131
  self.set_layer_labels(i, new_label)
125
132
 
126
133
  label_widget.double_clicked.connect(lambda: rename_label(label_widget, label, i))
127
- layout.addWidget(label_widget)
134
+ content_layout.addWidget(label_widget)
135
+ container_layout.addWidget(content_widget)
136
+ if is_current:
137
+ container.setStyleSheet(
138
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
139
+ else:
140
+ container.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
128
141
  item = QListWidgetItem()
129
- item.setSizeHint(QSize(gui_constants.IMG_WIDTH, gui_constants.IMG_HEIGHT))
142
+ item.setSizeHint(QSize(gui_constants.UI_SIZES['thumbnail_width'] + 4,
143
+ thumbnail.height() + label_widget.height() + 4))
130
144
  self.thumbnail_list.addItem(item)
131
- self.thumbnail_list.setItemWidget(item, item_widget)
132
-
145
+ self.thumbnail_list.setItemWidget(item, container)
133
146
  if is_current:
134
147
  self.thumbnail_list.setCurrentItem(item)
135
148
 
149
+ def highlight_thumbnail(self, index):
150
+ for i in range(self.thumbnail_list.count()):
151
+ item = self.thumbnail_list.item(i)
152
+ widget = self.thumbnail_list.itemWidget(item)
153
+ if widget:
154
+ widget.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
155
+ current_item = self.thumbnail_list.item(index)
156
+ if current_item:
157
+ widget = self.thumbnail_list.itemWidget(current_item)
158
+ if widget:
159
+ widget.setStyleSheet(
160
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
161
+ self.thumbnail_list.setCurrentRow(index)
162
+ self.thumbnail_list.scrollToItem(
163
+ self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
164
+
136
165
  def set_view_master(self):
137
166
  if self.has_no_master_layer():
138
167
  return
139
168
  self.view_mode = 'master'
140
169
  self.temp_view_individual = False
141
170
  self.display_master_layer()
171
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
172
+ self.highlight_thumbnail(self.current_layer_idx())
142
173
  self.status_message_requested.emit("View mode: Master")
143
174
  self.cursor_preview_state_changed.emit(True) # True = allow preview
144
175
 
@@ -148,6 +179,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
148
179
  self.view_mode = 'individual'
149
180
  self.temp_view_individual = False
150
181
  self.display_current_layer()
182
+ self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
183
+ self.highlight_thumbnail(self.current_layer_idx())
151
184
  self.status_message_requested.emit("View mode: Individual layers")
152
185
  self.cursor_preview_state_changed.emit(False) # False = no preview
153
186
 
@@ -155,6 +188,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
155
188
  if not self.temp_view_individual and self.view_mode == 'master':
156
189
  self.temp_view_individual = True
157
190
  self.image_viewer.update_brush_cursor()
191
+ self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
192
+ self.highlight_thumbnail(self.current_layer_idx())
158
193
  self.display_current_layer()
159
194
  self.status_message_requested.emit("Temporary view: Individual layer (hold X)")
160
195
 
@@ -162,6 +197,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
162
197
  if self.temp_view_individual:
163
198
  self.temp_view_individual = False
164
199
  self.image_viewer.update_brush_cursor()
200
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
201
+ self.highlight_thumbnail(self.current_layer_idx())
165
202
  self.display_master_layer()
166
203
  self.status_message_requested.emit("View mode: Master")
167
204
  self.cursor_preview_state_changed.emit(True) # Restore preview
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902
2
- from PySide6.QtWidgets import QMainWindow, QMessageBox, QAbstractItemView
2
+ from PySide6.QtWidgets import QMainWindow, QMessageBox
3
3
  from .. config.constants import constants
4
4
  from .undo_manager import UndoManager
5
5
  from .layer_collection import LayerCollection
@@ -87,7 +87,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
87
87
  def update_title(self):
88
88
  title = constants.APP_TITLE
89
89
  if self.io_gui_handler is not None:
90
- path = self.io_gui_handler.current_file_path
90
+ path = self.io_gui_handler.current_file_path()
91
91
  if path != '':
92
92
  title += f" - {path.split('/')[-1]}"
93
93
  if self.modified:
@@ -96,6 +96,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
96
96
 
97
97
  def mark_as_modified(self):
98
98
  self.modified = True
99
+ self.save_actions_set_enabled(True)
99
100
  self.update_title()
100
101
 
101
102
  def change_layer(self, layer_idx):
@@ -114,19 +115,14 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
114
115
  new_idx = max(0, self.current_layer_idx() - 1)
115
116
  if new_idx != self.current_layer_idx():
116
117
  self.change_layer(new_idx)
117
- self.highlight_thumbnail(new_idx)
118
+ self.display_manager.highlight_thumbnail(new_idx)
118
119
 
119
120
  def next_layer(self):
120
121
  if self.layer_stack() is not None:
121
122
  new_idx = min(self.number_of_layers() - 1, self.current_layer_idx() + 1)
122
123
  if new_idx != self.current_layer_idx():
123
124
  self.change_layer(new_idx)
124
- self.highlight_thumbnail(new_idx)
125
-
126
- def highlight_thumbnail(self, index):
127
- self.thumbnail_list.setCurrentRow(index)
128
- self.thumbnail_list.scrollToItem(
129
- self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
125
+ self.display_manager.highlight_thumbnail(new_idx)
130
126
 
131
127
  def copy_layer_to_master(self):
132
128
  if self.layer_stack() is None or self.master_layer() is None:
@@ -23,6 +23,7 @@ def brush_size_to_slider(size):
23
23
 
24
24
  class ImageEditorUI(ImageFilters):
25
25
  def __init__(self):
26
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
26
27
  super().__init__()
27
28
  self.brush = Brush()
28
29
  self.setup_ui()
@@ -125,16 +126,19 @@ class ImageEditorUI(ImageFilters):
125
126
  }
126
127
  """)
127
128
  master_label.setAlignment(Qt.AlignCenter)
128
- master_label.setFixedHeight(gui_constants.LABEL_HEIGHT)
129
+ master_label.setFixedHeight(gui_constants.UI_SIZES['label_height'])
129
130
  side_layout.addWidget(master_label)
130
131
  self.master_thumbnail_frame = QFrame()
132
+ self.master_thumbnail_frame.setObjectName("thumbnailContainer")
133
+ self.master_thumbnail_frame.setStyleSheet(
134
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
131
135
  self.master_thumbnail_frame.setFrameShape(QFrame.StyledPanel)
132
136
  master_thumbnail_layout = QVBoxLayout(self.master_thumbnail_frame)
133
- master_thumbnail_layout.setContentsMargins(2, 2, 2, 2)
137
+ master_thumbnail_layout.setContentsMargins(8, 8, 8, 8)
134
138
  self.master_thumbnail_label = QLabel()
135
139
  self.master_thumbnail_label.setAlignment(Qt.AlignCenter)
136
- self.master_thumbnail_label.setFixedSize(
137
- gui_constants.THUMB_WIDTH, gui_constants.THUMB_HEIGHT)
140
+ self.master_thumbnail_label.setFixedWidth(
141
+ gui_constants.UI_SIZES['thumbnail_width'])
138
142
  self.master_thumbnail_label.mousePressEvent = \
139
143
  lambda e: self.display_manager.set_view_master()
140
144
  master_thumbnail_layout.addWidget(self.master_thumbnail_label)
@@ -152,7 +156,7 @@ class ImageEditorUI(ImageFilters):
152
156
  }
153
157
  """)
154
158
  layers_label.setAlignment(Qt.AlignCenter)
155
- layers_label.setFixedHeight(gui_constants.LABEL_HEIGHT)
159
+ layers_label.setFixedHeight(gui_constants.UI_SIZES['label_height'])
156
160
  side_layout.addWidget(layers_label)
157
161
  self.thumbnail_list = QListWidget()
158
162
  self.thumbnail_list.setFocusPolicy(Qt.StrongFocus)
@@ -204,18 +208,29 @@ class ImageEditorUI(ImageFilters):
204
208
  layout.setSpacing(2)
205
209
  super().setup_ui()
206
210
 
211
+ def highlight_master_thumbnail(self):
212
+ self.master_thumbnail_frame.setStyleSheet(
213
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
214
+
207
215
  def setup_menu(self):
208
216
  menubar = self.menuBar()
209
217
  file_menu = menubar.addMenu("&File")
210
218
  file_menu.addAction("&Open...", self.io_gui_handler.open_file, "Ctrl+O")
211
- file_menu.addAction("&Save", self.io_gui_handler.save_file, "Ctrl+S")
212
- file_menu.addAction("Save &As...", self.io_gui_handler.save_file_as, "Ctrl+Shift+S")
213
- self.save_master_only = QAction("Save Master &Only", self)
214
- self.save_master_only.setCheckable(True)
215
- self.save_master_only.setChecked(True)
216
- file_menu.addAction(self.save_master_only)
217
-
218
- file_menu.addAction("&Close", self.io_gui_handler.close_file, "Ctrl+W")
219
+ self.save_action = QAction("&Save", self)
220
+ self.save_action.setShortcut("Ctrl+S")
221
+ self.save_action.triggered.connect(self.io_gui_handler.save_file)
222
+ file_menu.addAction(self.save_action)
223
+ self.save_as_action = QAction("Save &As...", self)
224
+ self.save_as_action.setShortcut("Ctrl+Shift+S")
225
+ self.save_as_action.triggered.connect(self.io_gui_handler.save_file_as)
226
+ file_menu.addAction(self.save_as_action)
227
+ self.io_gui_handler.save_master_only = QAction("Save Master &Only", self)
228
+ self.io_gui_handler.save_master_only.setCheckable(True)
229
+ self.io_gui_handler.save_master_only.setChecked(True)
230
+ file_menu.addAction(self.io_gui_handler.save_master_only)
231
+ self.save_actions_set_enabled(False)
232
+
233
+ file_menu.addAction("&Close", self.close_file, "Ctrl+W")
219
234
  file_menu.addSeparator()
220
235
  file_menu.addAction("&Import frames", self.io_gui_handler.import_frames)
221
236
  file_menu.addAction("Import &EXIF data", self.io_gui_handler.select_exif_path)
@@ -271,12 +286,12 @@ class ImageEditorUI(ImageFilters):
271
286
 
272
287
  view_master_action = QAction("View Master", self)
273
288
  view_master_action.setShortcut("M")
274
- view_master_action.triggered.connect(self.display_manager.set_view_master)
289
+ view_master_action.triggered.connect(self.set_view_master)
275
290
  view_menu.addAction(view_master_action)
276
291
 
277
292
  view_individual_action = QAction("View Individual", self)
278
293
  view_individual_action.setShortcut("L")
279
- view_individual_action.triggered.connect(self.display_manager.set_view_individual)
294
+ view_individual_action.triggered.connect(self.set_view_individual)
280
295
  view_menu.addAction(view_individual_action)
281
296
  view_menu.addSeparator()
282
297
 
@@ -334,6 +349,25 @@ class ImageEditorUI(ImageFilters):
334
349
  shortcuts_help_action.triggered.connect(self.shortcuts_help)
335
350
  help_menu.addAction(shortcuts_help_action)
336
351
 
352
+ def save_actions_set_enabled(self, enabled):
353
+ self.save_action.setEnabled(enabled)
354
+ self.save_as_action.setEnabled(enabled)
355
+ self.io_gui_handler.save_master_only.setEnabled(enabled)
356
+
357
+ def close_file(self):
358
+ self.io_gui_handler.close_file()
359
+ self.save_actions_set_enabled(False)
360
+
361
+ def set_view_master(self):
362
+ self.display_manager.set_view_master()
363
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
364
+ self.highlight_master_thumbnail()
365
+
366
+ def set_view_individual(self):
367
+ self.display_manager.set_view_individual()
368
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
369
+ self.highlight_master_thumbnail()
370
+
337
371
  def shortcuts_help(self):
338
372
  self._dialog = ShortcutsHelp(self)
339
373
  self._dialog.exec()
@@ -365,8 +399,12 @@ class ImageEditorUI(ImageFilters):
365
399
  def handle_temp_view(self, start):
366
400
  if start:
367
401
  self.display_manager.start_temp_view()
402
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_LO_COLOR
403
+ self.highlight_master_thumbnail()
368
404
  else:
369
405
  self.display_manager.end_temp_view()
406
+ self.thumbnail_highlight = gui_constants.THUMB_MASTER_HI_COLOR
407
+ self.highlight_master_thumbnail()
370
408
 
371
409
  def handle_brush_size_change(self, delta):
372
410
  if delta > 0: