mirror of
https://github.com/arsenetar/dupeguru-cocoa.git
synced 2026-01-22 06:37:18 +00:00
Initial commit
This commit is contained in:
0
cocoa/inter/__init__.py
Normal file
0
cocoa/inter/__init__.py
Normal file
10
cocoa/inter/all.py
Normal file
10
cocoa/inter/all.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from cocoa.inter import PyTextField, PyProgressWindow
|
||||
from .deletion_options import PyDeletionOptions
|
||||
from .details_panel import PyDetailsPanel
|
||||
from .directory_outline import PyDirectoryOutline
|
||||
from .prioritize_dialog import PyPrioritizeDialog
|
||||
from .prioritize_list import PyPrioritizeList
|
||||
from .problem_dialog import PyProblemDialog
|
||||
from .ignore_list_dialog import PyIgnoreListDialog
|
||||
from .result_table import PyResultTable
|
||||
from .stats_label import PyStatsLabel
|
||||
252
cocoa/inter/app.py
Normal file
252
cocoa/inter/app.py
Normal file
@@ -0,0 +1,252 @@
|
||||
import logging
|
||||
|
||||
from objp.util import pyref, dontwrap
|
||||
from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer
|
||||
from cocoa.inter import PyBaseApp, BaseAppView
|
||||
|
||||
import core.pe.photo
|
||||
from core.app import DupeGuru as DupeGuruBase, AppMode
|
||||
from .directories import Directories, Bundle
|
||||
from .photo import Photo
|
||||
|
||||
class DupeGuru(DupeGuruBase):
|
||||
PICTURE_CACHE_TYPE = 'shelve'
|
||||
|
||||
def __init__(self, view):
|
||||
DupeGuruBase.__init__(self, view)
|
||||
self.directories = Directories()
|
||||
|
||||
def selected_dupe_path(self):
|
||||
if not self.selected_dupes:
|
||||
return None
|
||||
return self.selected_dupes[0].path
|
||||
|
||||
def selected_dupe_ref_path(self):
|
||||
if not self.selected_dupes:
|
||||
return None
|
||||
ref = self.results.get_group_of_duplicate(self.selected_dupes[0]).ref
|
||||
if ref is self.selected_dupes[0]: # we don't want the same pic to be displayed on both sides
|
||||
return None
|
||||
return ref.path
|
||||
|
||||
def _get_fileclasses(self):
|
||||
result = DupeGuruBase._get_fileclasses(self)
|
||||
if self.app_mode == AppMode.Standard:
|
||||
result = [Bundle] + result
|
||||
return result
|
||||
|
||||
class DupeGuruView(BaseAppView):
|
||||
def askYesNoWithPrompt_(self, prompt: str) -> bool: pass
|
||||
def createResultsWindow(self): pass
|
||||
def showResultsWindow(self): pass
|
||||
def showProblemDialog(self): pass
|
||||
def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass
|
||||
def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass
|
||||
|
||||
class PyDupeGuru(PyBaseApp):
|
||||
@dontwrap
|
||||
def __init__(self):
|
||||
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = Photo
|
||||
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s')
|
||||
install_exception_hook('https://github.com/hsoft/dupeguru/issues')
|
||||
install_cocoa_logger()
|
||||
patch_threaded_job_performer()
|
||||
self.model = DupeGuru(self)
|
||||
|
||||
#---Sub-proxies
|
||||
def detailsPanel(self) -> pyref:
|
||||
return self.model.details_panel
|
||||
|
||||
def directoryTree(self) -> pyref:
|
||||
return self.model.directory_tree
|
||||
|
||||
def problemDialog(self) -> pyref:
|
||||
return self.model.problem_dialog
|
||||
|
||||
def statsLabel(self) -> pyref:
|
||||
return self.model.stats_label
|
||||
|
||||
def resultTable(self) -> pyref:
|
||||
return self.model.result_table
|
||||
|
||||
def ignoreListDialog(self) -> pyref:
|
||||
return self.model.ignore_list_dialog
|
||||
|
||||
def progressWindow(self) -> pyref:
|
||||
return self.model.progress_window
|
||||
|
||||
def deletionOptions(self) -> pyref:
|
||||
return self.model.deletion_options
|
||||
|
||||
#---Directories
|
||||
def addDirectory_(self, directory: str):
|
||||
self.model.add_directory(directory)
|
||||
|
||||
#---Results
|
||||
def doScan(self):
|
||||
self.model.start_scanning()
|
||||
|
||||
def exportToXHTML(self):
|
||||
self.model.export_to_xhtml()
|
||||
|
||||
def exportToCSV(self):
|
||||
self.model.export_to_csv()
|
||||
|
||||
def loadSession(self):
|
||||
self.model.load()
|
||||
|
||||
def loadResultsFrom_(self, filename: str):
|
||||
self.model.load_from(filename)
|
||||
|
||||
def markAll(self):
|
||||
self.model.mark_all()
|
||||
|
||||
def markNone(self):
|
||||
self.model.mark_none()
|
||||
|
||||
def markInvert(self):
|
||||
self.model.mark_invert()
|
||||
|
||||
def purgeIgnoreList(self):
|
||||
self.model.purge_ignore_list()
|
||||
|
||||
def toggleSelectedMark(self):
|
||||
self.model.toggle_selected_mark_state()
|
||||
|
||||
def saveSession(self):
|
||||
self.model.save()
|
||||
|
||||
def saveResultsAs_(self, filename: str):
|
||||
self.model.save_as(filename)
|
||||
|
||||
#---Actions
|
||||
def addSelectedToIgnoreList(self):
|
||||
self.model.add_selected_to_ignore_list()
|
||||
|
||||
def deleteMarked(self):
|
||||
self.model.delete_marked()
|
||||
|
||||
def applyFilter_(self, filter: str):
|
||||
self.model.apply_filter(filter)
|
||||
|
||||
def makeSelectedReference(self):
|
||||
self.model.make_selected_reference()
|
||||
|
||||
def copyMarked(self):
|
||||
self.model.copy_or_move_marked(copy=True)
|
||||
|
||||
def moveMarked(self):
|
||||
self.model.copy_or_move_marked(copy=False)
|
||||
|
||||
def openSelected(self):
|
||||
self.model.open_selected()
|
||||
|
||||
def removeMarked(self):
|
||||
self.model.remove_marked()
|
||||
|
||||
def removeSelected(self):
|
||||
self.model.remove_selected()
|
||||
|
||||
def revealSelected(self):
|
||||
self.model.reveal_selected()
|
||||
|
||||
def invokeCustomCommand(self):
|
||||
self.model.invoke_custom_command()
|
||||
|
||||
def showIgnoreList(self):
|
||||
self.model.ignore_list_dialog.show()
|
||||
|
||||
def clearPictureCache(self):
|
||||
self.model.clear_picture_cache()
|
||||
|
||||
#---Information
|
||||
def getScanOptions(self) -> list:
|
||||
return [o.label for o in self.model.SCANNER_CLASS.get_scan_options()]
|
||||
|
||||
def resultsAreModified(self) -> bool:
|
||||
return self.model.results.is_modified
|
||||
|
||||
def getSelectedDupePath(self) -> str:
|
||||
return str(self.model.selected_dupe_path())
|
||||
|
||||
def getSelectedDupeRefPath(self) -> str:
|
||||
return str(self.model.selected_dupe_ref_path())
|
||||
|
||||
#---Properties
|
||||
def getAppMode(self) -> int:
|
||||
return self.model.app_mode
|
||||
|
||||
def setAppMode_(self, app_mode: int):
|
||||
self.model.app_mode = app_mode
|
||||
|
||||
def setScanType_(self, scan_type_index: int):
|
||||
scan_options = self.model.SCANNER_CLASS.get_scan_options()
|
||||
try:
|
||||
so = scan_options[scan_type_index]
|
||||
self.model.options['scan_type'] = so.scan_type
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def setMinMatchPercentage_(self, percentage: int):
|
||||
self.model.options['min_match_percentage'] = int(percentage)
|
||||
|
||||
def setWordWeighting_(self, words_are_weighted: bool):
|
||||
self.model.options['word_weighting'] = words_are_weighted
|
||||
|
||||
def setMatchSimilarWords_(self, match_similar_words: bool):
|
||||
self.model.options['match_similar_words'] = match_similar_words
|
||||
|
||||
def setSizeThreshold_(self, size_threshold: int):
|
||||
self.model.options['size_threshold'] = size_threshold
|
||||
|
||||
def enable_scanForTag_(self, enable: bool, scan_tag: str):
|
||||
if 'scanned_tags' not in self.model.options:
|
||||
self.model.options['scanned_tags'] = set()
|
||||
if enable:
|
||||
self.model.options['scanned_tags'].add(scan_tag)
|
||||
else:
|
||||
self.model.options['scanned_tags'].discard(scan_tag)
|
||||
|
||||
def setMatchScaled_(self, match_scaled: bool):
|
||||
self.model.options['match_scaled'] = match_scaled
|
||||
|
||||
def setMixFileKind_(self, mix_file_kind: bool):
|
||||
self.model.options['mix_file_kind'] = mix_file_kind
|
||||
|
||||
def setEscapeFilterRegexp_(self, escape_filter_regexp: bool):
|
||||
self.model.options['escape_filter_regexp'] = escape_filter_regexp
|
||||
|
||||
def setRemoveEmptyFolders_(self, remove_empty_folders: bool):
|
||||
self.model.options['clean_empty_dirs'] = remove_empty_folders
|
||||
|
||||
def setIgnoreHardlinkMatches_(self, ignore_hardlink_matches: bool):
|
||||
self.model.options['ignore_hardlink_matches'] = ignore_hardlink_matches
|
||||
|
||||
def setCopyMoveDestType_(self, copymove_dest_type: int):
|
||||
self.model.options['copymove_dest_type'] = copymove_dest_type
|
||||
|
||||
#--- model --> view
|
||||
@dontwrap
|
||||
def ask_yes_no(self, prompt):
|
||||
return self.callback.askYesNoWithPrompt_(prompt)
|
||||
|
||||
@dontwrap
|
||||
def create_results_window(self):
|
||||
self.callback.createResultsWindow()
|
||||
|
||||
@dontwrap
|
||||
def show_results_window(self):
|
||||
self.callback.showResultsWindow()
|
||||
|
||||
@dontwrap
|
||||
def show_problem_dialog(self):
|
||||
self.callback.showProblemDialog()
|
||||
|
||||
@dontwrap
|
||||
def select_dest_folder(self, prompt):
|
||||
return self.callback.selectDestFolderWithPrompt_(prompt)
|
||||
|
||||
@dontwrap
|
||||
def select_dest_file(self, prompt, extension):
|
||||
return self.callback.selectDestFileWithPrompt_extension_(prompt, extension)
|
||||
|
||||
37
cocoa/inter/deletion_options.py
Normal file
37
cocoa/inter/deletion_options.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Created On: 2012-05-30
|
||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from objp.util import dontwrap
|
||||
from cocoa.inter import PyGUIObject, GUIObjectView
|
||||
|
||||
class DeletionOptionsView(GUIObjectView):
|
||||
def updateMsg_(self, msg: str): pass
|
||||
def show(self) -> bool: pass
|
||||
def setHardlinkOptionEnabled_(self, enabled: bool): pass
|
||||
|
||||
class PyDeletionOptions(PyGUIObject):
|
||||
def setLinkDeleted_(self, link_deleted: bool):
|
||||
self.model.link_deleted = link_deleted
|
||||
|
||||
def setUseHardlinks_(self, use_hardlinks: bool):
|
||||
self.model.use_hardlinks = use_hardlinks
|
||||
|
||||
def setDirect_(self, direct: bool):
|
||||
self.model.direct = direct
|
||||
|
||||
#--- model --> view
|
||||
@dontwrap
|
||||
def update_msg(self, msg):
|
||||
self.callback.updateMsg_(msg)
|
||||
|
||||
@dontwrap
|
||||
def show(self):
|
||||
return self.callback.show()
|
||||
|
||||
@dontwrap
|
||||
def set_hardlink_option_enabled(self, enabled):
|
||||
self.callback.setHardlinkOptionEnabled_(enabled)
|
||||
11
cocoa/inter/details_panel.py
Normal file
11
cocoa/inter/details_panel.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from cocoa.inter import PyGUIObject, GUIObjectView
|
||||
|
||||
class DetailsPanelView(GUIObjectView):
|
||||
pass
|
||||
|
||||
class PyDetailsPanel(PyGUIObject):
|
||||
def numberOfRows(self) -> int:
|
||||
return self.model.row_count()
|
||||
|
||||
def valueForColumn_row_(self, column: str, row: int) -> object:
|
||||
return self.model.row(row)[int(column)]
|
||||
53
cocoa/inter/directories.py
Normal file
53
cocoa/inter/directories.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from cocoa import proxy
|
||||
from hscommon.path import Path, pathify
|
||||
from core.se import fs
|
||||
from core.directories import Directories as DirectoriesBase, DirectoryState
|
||||
|
||||
def is_bundle(str_path):
|
||||
uti = proxy.getUTI_(str_path)
|
||||
if uti is None:
|
||||
logging.warning('There was an error trying to detect the UTI of %s', str_path)
|
||||
return proxy.type_conformsToType_(uti, 'com.apple.bundle') or proxy.type_conformsToType_(uti, 'com.apple.package')
|
||||
|
||||
class Bundle(fs.Folder):
|
||||
@classmethod
|
||||
@pathify
|
||||
def can_handle(cls, path: Path):
|
||||
return not path.islink() and path.isdir() and is_bundle(str(path))
|
||||
|
||||
class Directories(DirectoriesBase):
|
||||
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
|
||||
HOME_PATH_TO_EXCLUDE = [Path('Library')]
|
||||
|
||||
def _default_state_for_path(self, path):
|
||||
result = DirectoriesBase._default_state_for_path(self, path)
|
||||
if result is not None:
|
||||
return result
|
||||
if path in self.ROOT_PATH_TO_EXCLUDE:
|
||||
return DirectoryState.Excluded
|
||||
if path[:2] == Path('/Users') and path[3:] in self.HOME_PATH_TO_EXCLUDE:
|
||||
return DirectoryState.Excluded
|
||||
|
||||
def _get_folders(self, from_folder, j):
|
||||
# We don't want to scan bundle's subfolder even in Folders mode. Bundle's integrity has to
|
||||
# stay intact.
|
||||
if is_bundle(str(from_folder.path)):
|
||||
# just yield the current folder and bail
|
||||
state = self.get_state(from_folder.path)
|
||||
if state != DirectoryState.Excluded:
|
||||
from_folder.is_ref = state == DirectoryState.Reference
|
||||
yield from_folder
|
||||
return
|
||||
else:
|
||||
yield from DirectoriesBase._get_folders(self, from_folder, j)
|
||||
|
||||
@staticmethod
|
||||
def get_subfolders(path):
|
||||
result = DirectoriesBase.get_subfolders(path)
|
||||
return [p for p in result if not is_bundle(str(p))]
|
||||
21
cocoa/inter/directory_outline.py
Normal file
21
cocoa/inter/directory_outline.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from objp.util import dontwrap
|
||||
from cocoa.inter import PyOutline, GUIObjectView
|
||||
|
||||
class DirectoryOutlineView(GUIObjectView):
|
||||
pass
|
||||
|
||||
class PyDirectoryOutline(PyOutline):
|
||||
def addDirectory_(self, path: str):
|
||||
self.model.add_directory(path)
|
||||
|
||||
def removeSelectedDirectory(self):
|
||||
self.model.remove_selected()
|
||||
|
||||
def selectAll(self):
|
||||
self.model.select_all()
|
||||
|
||||
# python --> cocoa
|
||||
@dontwrap
|
||||
def refresh_states(self):
|
||||
# Under cocoa, both refresh() and refresh_states() do the same thing.
|
||||
self.callback.refresh()
|
||||
21
cocoa/inter/ignore_list_dialog.py
Normal file
21
cocoa/inter/ignore_list_dialog.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from objp.util import pyref, dontwrap
|
||||
from cocoa.inter import PyGUIObject, GUIObjectView
|
||||
|
||||
class IgnoreListDialogView(GUIObjectView):
|
||||
def show(self): pass
|
||||
|
||||
class PyIgnoreListDialog(PyGUIObject):
|
||||
def ignoreListTable(self) -> pyref:
|
||||
return self.model.ignore_list_table
|
||||
|
||||
def removeSelected(self):
|
||||
self.model.remove_selected()
|
||||
|
||||
def clear(self):
|
||||
self.model.clear()
|
||||
|
||||
#--- model --> view
|
||||
@dontwrap
|
||||
def show(self):
|
||||
self.callback.show()
|
||||
|
||||
35
cocoa/inter/photo.py
Normal file
35
cocoa/inter/photo.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from cocoa import proxy
|
||||
from core.pe import _block_osx
|
||||
from core.pe.photo import Photo as PhotoBase
|
||||
|
||||
class Photo(PhotoBase):
|
||||
HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy()
|
||||
HANDLED_EXTS.update({'psd', 'nef', 'cr2', 'orf'})
|
||||
|
||||
def _plat_get_dimensions(self):
|
||||
return _block_osx.get_image_size(str(self.path))
|
||||
|
||||
def _plat_get_blocks(self, block_count_per_side, orientation):
|
||||
try:
|
||||
blocks = _block_osx.getblocks(str(self.path), block_count_per_side, orientation)
|
||||
except Exception as e:
|
||||
raise IOError('The reading of "%s" failed with "%s"' % (str(self.path), str(e)))
|
||||
if not blocks:
|
||||
raise IOError('The picture %s could not be read' % str(self.path))
|
||||
return blocks
|
||||
|
||||
def _get_exif_timestamp(self):
|
||||
exifdata = proxy.readExifData_(str(self.path))
|
||||
if exifdata:
|
||||
try:
|
||||
return exifdata['{Exif}']['DateTimeOriginal']
|
||||
except KeyError:
|
||||
return ''
|
||||
else:
|
||||
return ''
|
||||
29
cocoa/inter/prioritize_dialog.py
Normal file
29
cocoa/inter/prioritize_dialog.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from objp.util import pyref
|
||||
from cocoa.inter import PyGUIObject, GUIObjectView
|
||||
from core.gui.prioritize_dialog import PrioritizeDialog
|
||||
|
||||
class PrioritizeDialogView(GUIObjectView):
|
||||
pass
|
||||
|
||||
class PyPrioritizeDialog(PyGUIObject):
|
||||
def __init__(self, app: pyref):
|
||||
model = PrioritizeDialog(app.model)
|
||||
PyGUIObject.__init__(self, model)
|
||||
|
||||
def categoryList(self) -> pyref:
|
||||
return self.model.category_list
|
||||
|
||||
def criteriaList(self) -> pyref:
|
||||
return self.model.criteria_list
|
||||
|
||||
def prioritizationList(self) -> pyref:
|
||||
return self.model.prioritization_list
|
||||
|
||||
def addSelected(self):
|
||||
self.model.add_selected()
|
||||
|
||||
def removeSelected(self):
|
||||
self.model.remove_selected()
|
||||
|
||||
def performReprioritization(self):
|
||||
self.model.perform_reprioritization()
|
||||
8
cocoa/inter/prioritize_list.py
Normal file
8
cocoa/inter/prioritize_list.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from cocoa.inter import PySelectableList, SelectableListView
|
||||
|
||||
class PrioritizeListView(SelectableListView):
|
||||
pass
|
||||
|
||||
class PyPrioritizeList(PySelectableList):
|
||||
def moveIndexes_toIndex_(self, indexes: list, dest_index: int):
|
||||
self.model.move_indexes(indexes, dest_index)
|
||||
9
cocoa/inter/problem_dialog.py
Normal file
9
cocoa/inter/problem_dialog.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from objp.util import pyref
|
||||
from cocoa.inter import PyGUIObject
|
||||
|
||||
class PyProblemDialog(PyGUIObject):
|
||||
def problemTable(self) -> pyref:
|
||||
return self.model.problem_table
|
||||
|
||||
def revealSelected(self):
|
||||
self.model.reveal_selected_dupe()
|
||||
50
cocoa/inter/result_table.py
Normal file
50
cocoa/inter/result_table.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from objp.util import dontwrap
|
||||
from cocoa.inter import PyTable, TableView
|
||||
|
||||
class ResultTableView(TableView):
|
||||
def invalidateMarkings(self): pass
|
||||
|
||||
class PyResultTable(PyTable):
|
||||
def powerMarkerMode(self) -> bool:
|
||||
return self.model.power_marker
|
||||
|
||||
def setPowerMarkerMode_(self, value: bool):
|
||||
self.model.power_marker = value
|
||||
|
||||
def deltaValuesMode(self) -> bool:
|
||||
return self.model.delta_values
|
||||
|
||||
def setDeltaValuesMode_(self, value: bool):
|
||||
self.model.delta_values = value
|
||||
|
||||
def valueForRow_column_(self, row_index: int, column: str) -> object:
|
||||
return self.model.get_row_value(row_index, column)
|
||||
|
||||
def isDeltaAtRow_column_(self, row_index: int, column: str) -> bool:
|
||||
row = self.model[row_index]
|
||||
return row.is_cell_delta(column)
|
||||
|
||||
def renameSelected_(self, newname: str) -> bool:
|
||||
return self.model.rename_selected(newname)
|
||||
|
||||
def sortBy_ascending_(self, key: str, asc: bool):
|
||||
self.model.sort(key, asc)
|
||||
|
||||
def markSelected(self):
|
||||
self.model.app.toggle_selected_mark_state()
|
||||
|
||||
def removeSelected(self):
|
||||
self.model.app.remove_selected()
|
||||
|
||||
def selectedDupeCount(self) -> int:
|
||||
return self.model.selected_dupe_count
|
||||
|
||||
def pathAtIndex_(self, index: int) -> str:
|
||||
row = self.model[index]
|
||||
return str(row._dupe.path)
|
||||
|
||||
# python --> cocoa
|
||||
@dontwrap
|
||||
def invalidate_markings(self):
|
||||
self.callback.invalidateMarkings()
|
||||
|
||||
9
cocoa/inter/stats_label.py
Normal file
9
cocoa/inter/stats_label.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from cocoa.inter import PyGUIObject, GUIObjectView
|
||||
|
||||
class StatsLabelView(GUIObjectView):
|
||||
pass
|
||||
|
||||
class PyStatsLabel(PyGUIObject):
|
||||
|
||||
def display(self) -> str:
|
||||
return self.model.display
|
||||
Reference in New Issue
Block a user