.
675 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #! /usr/bin/make -f
2 |
3 | # Git-Annex-Metadata-Gui
4 | # Copyright (C) 2017 Alper Nebi Yasak
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | QTUI:=$(wildcard qtdesigner-ui/*.ui)
20 | PYUI:=$(QTUI:qtdesigner-ui/%.ui=git_annex_metadata_gui/%_ui.py)
21 |
22 | git_annex_metadata_gui/%_ui.py: qtdesigner-ui/%.ui
23 | pyuic5 -o $@ $<
24 |
25 | all: gui
26 |
27 | gui: $(PYUI)
28 |
29 | design:
30 | PYQTDESIGNERPATH=qtdesigner-plugins \
31 | PYTHONPATH=git_annex_metadata_gui \
32 | designer qtdesigner-ui/main_window.ui \
33 | >/dev/null 2>&1 &
34 |
35 | design-plugins:
36 | PYQTDESIGNERPATH=qtdesigner-plugins \
37 | PYTHONPATH=git_annex_metadata_gui \
38 | designer
39 |
40 | test:
41 | python3 -m "unittest" -vb
42 |
43 | .PHONY: all gui design design-plugins test
44 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ======================
2 | Git-Annex-Metadata-Gui
3 | ======================
4 | A graphical interface to the metadata functionality of git-annex_.
5 |
6 | .. _git-annex: https://git-annex.branchable.com/
7 |
8 | Requirements
9 | ------------
10 | - Python 3
11 | - git-annex-adapter_
12 | - PyQt5
13 |
14 | .. _git-annex-adapter: https://github.com/alpernebbi/git-annex-adapter
15 |
16 | Usage
17 | -----
18 | ::
19 |
20 | usage: git-annex-metadata-gui [option ...] [repo-path]
21 |
22 | A graphical interface for git-annex metadata.
23 |
24 | positional arguments:
25 | repo-path path of the git-annex repository
26 |
27 | optional arguments:
28 | -h, --help show this help message and exit
29 | -v, --version print version information and exit
30 | --debug print debug-level log messages
31 | --full-load don't load models incrementially
32 |
33 | Also see the manual entry for qt5options(7)
34 |
35 | Screenshots
36 | -----------
37 |
38 | .. image:: https://github.com/alpernebbi/
39 | git-annex-metadata-gui/wiki/screenshots/v020s1.png
40 | :alt: Workflow with a maximized window, both docks visible.
41 |
42 | See `the wiki page`_ for more screenshots.
43 |
44 | .. _the wiki page: https://github.com/alpernebbi/
45 | git-annex-metadata-gui/wiki/Screenshots
46 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Git-Annex-Metadata-Gui
4 | # Copyright (C) 2017 Alper Nebi Yasak
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import argparse
20 | import sys
21 | import logging
22 |
23 | from PyQt5 import Qt
24 | from PyQt5 import QtCore
25 | from PyQt5 import QtGui
26 | from PyQt5 import QtWidgets
27 |
28 | from .utils import AutoConsumed
29 | from .utils import StatusBarLogHandler
30 | from .main_window import MainWindow
31 |
32 | app = None
33 |
34 | logger = logging.getLogger(__name__)
35 |
36 | def main():
37 | global app
38 | app = QtWidgets.QApplication(sys.argv)
39 | my_args = parse_args(app.arguments())
40 |
41 | main_window = MainWindow()
42 | setup_logger(main_window, debug=my_args.debug)
43 |
44 | if my_args.full_load:
45 | AutoConsumed._timeout = float('inf')
46 |
47 | if my_args.repo_path:
48 | QtCore.QMetaObject.invokeMethod(
49 | main_window, 'open_repo',
50 | Qt.Qt.QueuedConnection,
51 | QtCore.Q_ARG(str, my_args.repo_path),
52 | )
53 |
54 | main_window.show()
55 | return app.exec_()
56 |
57 |
58 | def setup_logger(main_window, debug=False):
59 | stderr_handler = logging.StreamHandler(sys.stderr)
60 | stderr_handler.setLevel(logging.WARNING)
61 |
62 | stderr_formatter = logging.Formatter(
63 | fmt='[{asctime}] [{name}] [{levelname}]: {message}',
64 | style='{',
65 | )
66 | stderr_handler.setFormatter(stderr_formatter)
67 |
68 | statusbar_handler = StatusBarLogHandler(main_window.statusBar())
69 | statusbar_handler.setLevel(logging.INFO)
70 | statusbar_formatter = logging.Formatter(
71 | fmt='{message}',
72 | style='{',
73 | )
74 | statusbar_handler.setFormatter(statusbar_formatter)
75 |
76 | root_logger = logging.getLogger()
77 | root_logger.addHandler(stderr_handler)
78 | root_logger.addHandler(statusbar_handler)
79 | root_logger.setLevel(logging.INFO)
80 |
81 | if debug:
82 | stderr_handler.setLevel(logging.DEBUG)
83 | root_logger.setLevel(logging.DEBUG)
84 | logger.debug('Enabled debug messages')
85 |
86 | def excepthook(exc_type, value, traceback):
87 | exc_info = (exc_type, value, traceback)
88 | logger.critical('%s', value, exc_info=exc_info)
89 | sys.excepthook = excepthook
90 |
91 |
92 | def parse_args(argv):
93 | parser = argparse.ArgumentParser(
94 | description="A graphical interface for git-annex metadata.",
95 | usage="%(prog)s [option ...] [repo-path]",
96 | epilog="Also see the manual entry for qt5options(7)",
97 | add_help=True,
98 | )
99 |
100 | parser.add_argument(
101 | "repo_path",
102 | metavar='repo-path',
103 | nargs='?',
104 | help="path of the git-annex repository",
105 | )
106 |
107 | parser.add_argument(
108 | "-v", "--version",
109 | action='version',
110 | version="%(prog)s v0.2.0",
111 | help="print version information and exit",
112 | )
113 |
114 | parser.add_argument(
115 | "--debug",
116 | action='store_true',
117 | help="print debug-level log messages",
118 | )
119 |
120 | parser.add_argument(
121 | "--full-load",
122 | action='store_true',
123 | help="don't load models incrementially",
124 | )
125 |
126 | return parser.parse_args(argv[1:])
127 |
128 |
129 | if __name__ == "__main__":
130 | main()
131 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/auto_size_line_edit.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import logging
18 |
19 | from PyQt5 import QtCore
20 | from PyQt5 import QtWidgets
21 |
22 | logger = logging.getLogger(__name__)
23 |
24 | class AutoSizeLineEdit(QtWidgets.QLineEdit):
25 | def __init__(self, parent=None):
26 | super().__init__(parent)
27 | self.textChanged.connect(self.updateGeometry)
28 |
29 | def sizeHint(self):
30 | height = super().sizeHint().height()
31 | min_width = self.minimumSizeHint().width()
32 | text_width = self.fontMetrics().width(self.text())
33 |
34 | width = text_width + min_width
35 | if not self.isVisible() and self.isClearButtonEnabled():
36 | width += 26
37 |
38 | return QtCore.QSize(width, height)
39 |
40 | def __repr__(self):
41 | return "{name}.{cls}({args})".format(
42 | name=__name__,
43 | cls=self.__class__.__name__,
44 | args='',
45 | )
46 |
47 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/field_item_edit.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import logging
18 |
19 | from PyQt5 import Qt
20 | from PyQt5 import QtCore
21 | from PyQt5 import QtWidgets
22 |
23 | try:
24 | from .auto_size_line_edit import AutoSizeLineEdit
25 | except ImportError:
26 | from auto_size_line_edit import AutoSizeLineEdit
27 |
28 | logger = logging.getLogger(__name__)
29 |
30 |
31 | class FieldItemEdit(QtWidgets.QWidget):
32 | cleared = QtCore.pyqtSignal()
33 |
34 | def __init__(self, item, parent=None):
35 | super().__init__(parent)
36 | self._item = item
37 | self._values = []
38 |
39 | layout = QtWidgets.QHBoxLayout(self)
40 | layout.setContentsMargins(0, 0, 0, 0)
41 | self.setLayout(layout)
42 |
43 | model = self._item.model()
44 | model.dataChanged.connect(self._on_data_changed)
45 |
46 | append_button = QtWidgets.QPushButton()
47 | append_button.setText('+')
48 | append_button.setMaximumWidth(32)
49 | append_button.clicked.connect(self._on_append_button_clicked)
50 | self.layout().addWidget(append_button)
51 |
52 | self.update_widgets()
53 |
54 | def widget_count(self):
55 | return self.layout().count() - 1
56 |
57 | def _on_data_changed(self, topLeft, bottomRight, roles):
58 | rows = range(topLeft.row(), bottomRight.row() + 1)
59 | columns = range(topLeft.column(), bottomRight.column() + 1)
60 |
61 | if self._item.row() in rows and self._item.column() in columns:
62 | self.update_widgets()
63 |
64 | def create_widget(self):
65 | widget = AutoSizeLineEdit()
66 | widget.editingFinished.connect(self._on_editing_finished)
67 | widget.setClearButtonEnabled(True)
68 | widget.setAlignment(Qt.Qt.AlignCenter)
69 | return widget
70 |
71 | def update_widgets(self):
72 | values = self._item.data(Qt.Qt.UserRole)
73 |
74 | for v in set(self._values) - values:
75 | self._values.remove(v)
76 |
77 | new_values = sorted(values - set(self._values))
78 | self._values.extend(new_values)
79 |
80 | while self.widget_count() > len(self._values):
81 | child = self.layout().takeAt(self.widget_count() - 1)
82 | widget = child.widget()
83 | widget.deleteLater()
84 |
85 | if self.widget_count() == 0:
86 | self.cleared.emit()
87 |
88 | while self.widget_count() < len(self._values):
89 | widget = self.create_widget()
90 | self.layout().insertWidget(self.widget_count(), widget)
91 |
92 | for idx in range(self.widget_count()):
93 | widget = self.layout().itemAt(idx).widget()
94 | if idx < len(self._values):
95 | widget.setText(self._values[idx])
96 | else:
97 | widget.setText('')
98 |
99 | for idx in range(1, self.layout().count()):
100 | left = self.layout().itemAt(idx - 1).widget()
101 | right = self.layout().itemAt(idx).widget()
102 | self.setTabOrder(left, right)
103 |
104 | def _on_editing_finished(self):
105 | values = []
106 | for idx in range(self.widget_count()):
107 | value = self.layout().itemAt(idx).widget().text()
108 | if value:
109 | values.append(value)
110 | if idx < len(self._values) and value != self._values[idx]:
111 | self._values[idx] = value
112 |
113 | self._item.setData(set(values), role=Qt.Qt.UserRole)
114 |
115 | def _on_append_button_clicked(self):
116 | button_idx = self.widget_count()
117 | if button_idx == 0:
118 | create = True
119 | else:
120 | widget_idx = button_idx - 1
121 | last_widget = self.layout().itemAt(widget_idx).widget()
122 | create = bool(last_widget.text())
123 |
124 | if create:
125 | widget = self.create_widget()
126 | self.layout().insertWidget(button_idx, widget)
127 | else:
128 | widget = last_widget
129 |
130 | widget.setFocus()
131 |
132 | def __repr__(self):
133 | return "{name}.{cls}({args})".format(
134 | name=__name__,
135 | cls=self.__class__.__name__,
136 | args=self._item,
137 | )
138 |
139 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/file_metadata_model.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import collections
18 | import logging
19 | import random
20 | import pygit2
21 |
22 | from PyQt5 import Qt
23 | from PyQt5 import QtCore
24 | from PyQt5 import QtGui
25 | from PyQt5 import QtWidgets
26 |
27 | from git_annex_adapter.repo import AnnexedFile
28 | from git_annex_adapter.repo import AnnexedFileTree
29 |
30 | from .utils import AutoConsumed
31 | from .utils import DataProxyItem
32 |
33 | logger = logging.getLogger(__name__)
34 |
35 |
36 | class AnnexedFileItem(DataProxyItem):
37 | def __init__(self, key_item, filename):
38 | super().__init__(key_item)
39 | self._name = filename
40 |
41 | self.setSelectable(True)
42 | self.setEditable(False)
43 | self.setEnabled(True)
44 | self.setFlags(self.flags() | Qt.Qt.ItemNeverHasChildren)
45 |
46 | @property
47 | def key(self):
48 | return self._item.key
49 |
50 | @property
51 | def name(self):
52 | return self._name
53 |
54 | @property
55 | def contentlocation(self):
56 | return self._item.contentlocation
57 |
58 | def type(self):
59 | return QtGui.QStandardItem.UserType + 4
60 |
61 | def data(self, role=Qt.Qt.DisplayRole):
62 | if role == Qt.Qt.DisplayRole:
63 | return self._name
64 | if role == Qt.Qt.ToolTipRole:
65 | return self._name
66 | if role == Qt.Qt.FontRole:
67 | return QtGui.QStandardItem.data(self, role=role)
68 | else:
69 | return super().data(role=role)
70 |
71 | def __lt__(self, other):
72 | if other is None:
73 | return True
74 |
75 | elif isinstance(other, AnnexedFileItem):
76 | return super().__lt__(other)
77 |
78 | elif isinstance(other, AnnexedDirectoryItem):
79 | return False
80 |
81 | else:
82 | return NotImplemented
83 |
84 | def __repr__(self):
85 | return "{name}.{cls}({args})".format(
86 | name=__name__,
87 | cls=self.__class__.__name__,
88 | args={
89 | 'item': self._item,
90 | 'name': self._name,
91 | },
92 | )
93 |
94 |
95 | class AnnexedFileFieldItem(DataProxyItem):
96 | def __init__(self, field_item, filename):
97 | super().__init__(field_item)
98 | self._name = filename
99 |
100 | @property
101 | def key(self):
102 | return self._item.key
103 |
104 | @property
105 | def name(self):
106 | return self._name
107 |
108 | @property
109 | def contentlocation(self):
110 | return self._item.contentlocation
111 |
112 | def __lt__(self, other):
113 | if other is None:
114 | return True
115 |
116 | elif isinstance(other, AnnexedFileFieldItem):
117 | lhs = self.data(role=Qt.Qt.UserRole)
118 | rhs = other.data(role=Qt.Qt.UserRole)
119 | if len(lhs) == 0:
120 | return False
121 | elif len(rhs) == 0:
122 | return True
123 | elif len(lhs) == len(rhs) == 1:
124 | lhs_, rhs_ = lhs.pop(), rhs.pop()
125 | try:
126 | return int(lhs_) > int(rhs_)
127 | except ValueError:
128 | return super().__lt__(other)
129 | else:
130 | return len(lhs) > len(rhs)
131 |
132 | elif isinstance(other, AnnexedDirectoryFieldItem):
133 | return True
134 |
135 | else:
136 | return NotImplemented
137 |
138 |
139 | class AnnexedDirectoryItem(QtGui.QStandardItem):
140 | def __init__(self, dirname):
141 | super().__init__()
142 | self._name = dirname
143 |
144 | self.setText(self._name)
145 | self.setToolTip(self._name)
146 |
147 | icon = QtWidgets.QFileIconProvider.Folder
148 | icon = QtWidgets.QFileIconProvider().icon(icon)
149 | self.setIcon(icon)
150 |
151 | self.setSelectable(True)
152 | self.setEditable(False)
153 | self.setEnabled(True)
154 |
155 | def type(self):
156 | return QtGui.QStandardItem.UserType + 6
157 |
158 | def __lt__(self, other):
159 | if other is None:
160 | return True
161 |
162 | elif isinstance(other, AnnexedFileItem):
163 | return True
164 |
165 | elif isinstance(other, AnnexedDirectoryItem):
166 | return super().__lt__(other)
167 |
168 | else:
169 | return NotImplemented
170 |
171 | def __repr__(self):
172 | return "{name}.{cls}({args})".format(
173 | name=__name__,
174 | cls=self.__class__.__name__,
175 | args=self._name,
176 | )
177 |
178 |
179 | class AnnexedDirectoryFieldItem(QtGui.QStandardItem):
180 | def __init__(self, dir_item):
181 | super().__init__()
182 | self._item = dir_item
183 | self._connected = False
184 | self._column_data_cache = {}
185 |
186 | self.setSelectable(True)
187 | self.setEditable(False)
188 | self.setEnabled(True)
189 | self.setFlags(self.flags() | Qt.Qt.ItemNeverHasChildren)
190 |
191 | if self._item.model():
192 | self._connect()
193 |
194 | def _connect(self):
195 | if self._connected:
196 | return
197 |
198 | model = self._item.model()
199 | model.dataChanged.connect(self._propagate_changes)
200 | model.layoutChanged.connect(self._emit_data_changed)
201 | model.modelReset.connect(self._emit_data_changed)
202 | model.rowsInserted.connect(self._on_rows_inserted)
203 | model.rowsMoved.connect(self._on_rows_moved)
204 | model.rowsRemoved.connect(self._on_rows_removed)
205 | model.columnsInserted.connect(self._on_columns_inserted)
206 | model.columnsMoved.connect(self._on_columns_moved)
207 | model.columnsRemoved.connect(self._on_columns_removed)
208 |
209 | self._connected = True
210 |
211 | def type(self):
212 | return QtGui.QStandardItem.UserType + 7
213 |
214 | def data(self, role=Qt.Qt.DisplayRole):
215 | if role == Qt.Qt.DisplayRole:
216 | return self._column_data(role=role)
217 |
218 | elif role == Qt.Qt.ToolTipRole:
219 | return self._column_data(role=role)
220 |
221 | else:
222 | return super().data(role=role)
223 |
224 | def _column_data(self, role=Qt.Qt.DisplayRole):
225 | if role in self._column_data_cache:
226 | return self._column_data_cache[role]
227 |
228 | children = (
229 | self._item.child(row, self.column())
230 | for row in range(self._item.rowCount())
231 | )
232 |
233 | responses = set()
234 | for child in children:
235 | if child:
236 | responses.add(child.data(role=role))
237 | if len(responses) > 1:
238 | responses.clear()
239 | break
240 |
241 | if responses:
242 | data = responses.pop()
243 | else:
244 | data = None
245 |
246 | self._column_data_cache[role] = data
247 | return data
248 |
249 | def _propagate_changes(self, topLeft, bottomRight, roles):
250 | rows = range(topLeft.row(), bottomRight.row() + 1)
251 | columns = range(topLeft.column(), bottomRight.column() + 1)
252 |
253 | parent = topLeft.parent()
254 | if parent != bottomRight.parent():
255 | self._emit_data_changed()
256 | return
257 |
258 | if parent != self._item.index():
259 | return
260 |
261 | if self.column() in columns:
262 | self._emit_data_changed()
263 |
264 | def _emit_data_changed(self):
265 | self._column_data_cache.clear()
266 | self.emitDataChanged()
267 |
268 | def _on_rows_inserted(self, parent, first, last):
269 | if parent == self._item.index():
270 | self._emit_data_changed()
271 |
272 | def _on_rows_moved(self, parent, start, end, destination, row):
273 | if parent == self._item.index():
274 | self._emit_data_changed()
275 | if destination == self._item.index():
276 | self._emit_data_changed()
277 |
278 | def _on_rows_removed(self, parent, first, last):
279 | if parent == self._item.index():
280 | self._emit_data_changed()
281 |
282 | def _on_columns_inserted(self, parent, first, last):
283 | if parent == self._item.index():
284 | self._emit_data_changed()
285 |
286 | def _on_columns_moved(self, parent, start, end, destination, col):
287 | if parent == self._item.index():
288 | self._emit_data_changed()
289 | if destination == self._item.index():
290 | self._emit_data_changed()
291 |
292 | def _on_columns_removed(self, parent, first, last):
293 | if parent == self._item.index():
294 | self._emit_data_changed()
295 |
296 | def __lt__(self, other):
297 | if other is None:
298 | return True
299 |
300 | elif isinstance(other, AnnexedFileFieldItem):
301 | return True
302 |
303 | elif isinstance(other, AnnexedDirectoryFieldItem):
304 | lhs = self.data(role=Qt.Qt.UserRole)
305 | rhs = other.data(role=Qt.Qt.UserRole)
306 | if len(lhs) == 0:
307 | return False
308 | elif len(rhs) == 0:
309 | return True
310 | elif len(lhs) == len(rhs) == 1:
311 | lhs_, rhs_ = lhs.pop(), rhs.pop()
312 | try:
313 | return int(lhs_) > int(rhs_)
314 | except ValueError:
315 | return super().__lt__(other)
316 | else:
317 | return len(lhs) > len(rhs)
318 |
319 | else:
320 | return NotImplemented
321 |
322 | def __repr__(self):
323 | return "{name}.{cls}({args})".format(
324 | name=__name__,
325 | cls=self.__class__.__name__,
326 | args={
327 | 'item': self._item,
328 | 'column': self.column(),
329 | }
330 | )
331 |
332 |
333 | class AnnexedFileMetadataModel(QtGui.QStandardItemModel):
334 | def __init__(self, parent=None):
335 | super().__init__(parent)
336 | self._treeish = None
337 | self._pending_files = collections.defaultdict(list)
338 |
339 | def setSourceModel(self, model):
340 | self._model = model
341 |
342 | model.columnsInserted.connect(self._on_columns_inserted)
343 | model.headerDataChanged.connect(self._on_header_data_changed)
344 | model.modelReset.connect(self.setTreeish)
345 | model.key_inserted.connect(self._on_key_inserted)
346 |
347 | if self._model.repo:
348 | self.setTreeish()
349 |
350 | @property
351 | def fields(self):
352 | return self._model.fields
353 |
354 | @QtCore.pyqtSlot(str)
355 | def insert_field(self, field):
356 | return self._model.insert_field(field)
357 |
358 | @QtCore.pyqtSlot()
359 | @QtCore.pyqtSlot(str)
360 | def setTreeish(self, treeish=None):
361 | if treeish is None:
362 | treeish = self._treeish
363 |
364 | if treeish is None:
365 | treeish = 'HEAD'
366 |
367 | if self._build_tree.running():
368 | msg = "Aborted loading previous tree model."
369 | logger.info(msg)
370 | self._build_tree.stop()
371 |
372 | self._treeish = treeish
373 | self._pending_files = collections.defaultdict(list)
374 | self.clear()
375 |
376 | headers = ['Filename', *self._model.fields[1:]]
377 | self.setHorizontalHeaderLabels(headers)
378 |
379 | self._build_tree.start()
380 |
381 | @QtCore.pyqtSlot()
382 | @AutoConsumed
383 | def _build_tree(self):
384 | msg = "Loading tree model..."
385 | logger.info(msg)
386 |
387 | PendingObject = collections.namedtuple(
388 | 'PendingObject',
389 | ['object', 'name', 'parent'],
390 | )
391 |
392 | pending = collections.deque()
393 |
394 | root = self._model.repo.annex.get_file_tree(self._treeish)
395 | root_item = self.invisibleRootItem()
396 | for name_, obj_ in root.items():
397 | p = PendingObject(obj_, name_, root_item)
398 | pending.append(p)
399 | yield
400 |
401 | while pending:
402 | obj, name, parent = pending.pop()
403 |
404 | if isinstance(obj, pygit2.Blob):
405 | pass
406 |
407 | elif isinstance(obj, AnnexedFileTree):
408 | item = AnnexedDirectoryItem(name)
409 | field_items = [
410 | AnnexedDirectoryFieldItem(item)
411 | for c in range(1, self._model.columnCount())
412 | ]
413 | parent.appendRow([item, *field_items])
414 |
415 | for field_item in field_items:
416 | p = PendingObject(field_item, name, parent)
417 | pending.append(p)
418 |
419 | for name_, obj_ in obj.items():
420 | p = PendingObject(obj_, name_, item)
421 | pending.append(p)
422 |
423 | elif isinstance(obj, AnnexedFile):
424 | if obj.key in self._model.key_items:
425 | key_item = self._model.key_items[obj.key]
426 | self.insert_file(key_item, name, parent)
427 | else:
428 | f = PendingObject(obj, name, parent)
429 | self._pending_files[obj.key].append(f)
430 |
431 | elif isinstance(obj, AnnexedDirectoryFieldItem):
432 | obj._connect()
433 |
434 | yield
435 |
436 | if self._pending_files:
437 | msg = "Tree model folders loaded, waiting for key model..."
438 | else:
439 | msg = "Tree model fully loaded."
440 | logger.info(msg)
441 |
442 | def insert_file(self, key_item, name, parent=None):
443 | if parent is None:
444 | parent = self.invisibleRootItem()
445 |
446 | file_item = AnnexedFileItem(key_item, name)
447 |
448 | def file_field_item(col):
449 | field_item = self._model.item(key_item.row(), col)
450 | return AnnexedFileFieldItem(field_item, name)
451 |
452 | file_field_items = (
453 | file_field_item(c)
454 | for c in range(1, self._model.columnCount())
455 | )
456 |
457 | parent.appendRow([file_item, *file_field_items])
458 |
459 | def _on_key_inserted(self, key):
460 | for (_, name, parent) in self._pending_files[key]:
461 | key_item = self._model.key_items[key]
462 | self.insert_file(key_item, name, parent)
463 | self._pending_files[key].clear()
464 |
465 | def _on_columns_inserted(self, parent, first, last):
466 | columns = range(first, last + 1)
467 |
468 | if first == 0:
469 | return
470 |
471 | for col in columns:
472 | self._create_column(col)
473 |
474 | def _create_column(self, col, parent=None):
475 | if parent is None:
476 | parent = self.invisibleRootItem()
477 |
478 | def _create_field(item):
479 | if isinstance(item, AnnexedFileItem):
480 | obj = self._model.key_items.get(item.key)
481 | field_item = self._model.item(obj.row(), col)
482 | return AnnexedFileFieldItem(field_item, item.name)
483 |
484 | elif isinstance(item, AnnexedDirectoryItem):
485 | return AnnexedDirectoryFieldItem(item)
486 |
487 | field_items = [
488 | _create_field(parent.child(i))
489 | for i in range(parent.rowCount())
490 | ]
491 | parent.insertColumn(col, field_items)
492 |
493 | for i in range(parent.rowCount()):
494 | child = parent.child(i)
495 | if isinstance(child, AnnexedDirectoryItem):
496 | self._create_column(col, parent=child)
497 |
498 | def _on_header_data_changed(self, orientation, first, last):
499 | if orientation == Qt.Qt.Horizontal:
500 | labels = ['Filename', *self._model.fields[1:]]
501 | self.setHorizontalHeaderLabels(labels)
502 |
503 | def __repr__(self):
504 | return "{name}.{cls}({args})".format(
505 | name=__name__,
506 | cls=self.__class__.__name__,
507 | args=self._model,
508 | )
509 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/file_preview.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import logging
18 | import mimetypes
19 |
20 | from PyQt5 import Qt
21 | from PyQt5 import QtGui
22 | from PyQt5 import QtCore
23 | from PyQt5 import QtWidgets
24 |
25 | logger = logging.getLogger(__name__)
26 |
27 |
28 | class FilePreview(QtWidgets.QStackedWidget):
29 | def __init__(self, parent=None):
30 | super().__init__(parent)
31 |
32 | # These are set by Qt Designer
33 | self.text_preview = None
34 | self.graphics_preview = None
35 |
36 | def addWidget(self, widget):
37 | super().addWidget(widget)
38 | if isinstance(widget, QtWidgets.QPlainTextEdit):
39 | self.text_preview = widget
40 | if isinstance(widget, QtWidgets.QGraphicsView):
41 | self.graphics_preview = widget
42 |
43 | @QtCore.pyqtSlot()
44 | def clear(self):
45 | if self.text_preview is not None:
46 | self.text_preview.clear()
47 |
48 | if self.graphics_preview is not None:
49 | old_scene = self.graphics_preview.scene()
50 | if old_scene:
51 | old_scene.clear()
52 | old_scene.deleteLater()
53 |
54 | @QtCore.pyqtSlot(str)
55 | def preview_text_file(self, path):
56 | filename = path.split('/')[-1]
57 |
58 | if self.text_preview is None:
59 | msg = "Text preview widget not created yet."
60 | logger.critical(msg)
61 | return
62 |
63 | if not self.isVisible():
64 | msg = "Preview widget invisible, not previewing text."
65 | logger.info(msg)
66 | return
67 |
68 | self.setCurrentWidget(self.text_preview)
69 |
70 | try:
71 | with open(path, 'r') as file:
72 | text = file.read()
73 | except UnicodeDecodeError:
74 | fmt = "File '{}' should be a UTF-8 text file, but isn't."
75 | msg = fmt.format(filename)
76 | logger.error(msg)
77 | return
78 |
79 | self.text_preview.setPlainText(text)
80 |
81 | fmt = "Previewed file '{}' as plain text."
82 | msg = fmt.format(filename)
83 | logger.info(msg)
84 |
85 | @QtCore.pyqtSlot(str)
86 | def preview_image_file(self, path):
87 | filename = path.split('/')[-1]
88 |
89 | if self.graphics_preview is None:
90 | msg = "Graphics preview widget not created yet."
91 | logger.critical(msg)
92 | return
93 |
94 | if not self.isVisible():
95 | msg = "Preview widget invisible, not previewing image."
96 | logger.info(msg)
97 | return
98 |
99 | self.setCurrentWidget(self.graphics_preview)
100 |
101 | scene = QtWidgets.QGraphicsScene(self)
102 | self.graphics_preview.setScene(scene)
103 |
104 | # Using QImage instead of directly creating the QPixmap
105 | # prevents a segmentation fault in my container setup
106 | image = QtGui.QImage(path)
107 | if image.isNull():
108 | fmt = "File '{}' should be an image, but isn't."
109 | msg = fmt.format(filename)
110 | logger.error(msg)
111 | return
112 |
113 | pixmap = QtGui.QPixmap.fromImage(image)
114 | if pixmap.isNull():
115 | fmt = "Failed to generate pixmap from image '{}'."
116 | msg = fmt.format(filename)
117 | logger.critical(msg)
118 | return
119 |
120 | pixmap_item = QtWidgets.QGraphicsPixmapItem(pixmap)
121 | scene.addItem(pixmap_item)
122 | self.graphics_preview.fitInView(
123 | pixmap_item,
124 | Qt.Qt.KeepAspectRatio,
125 | )
126 |
127 | fmt = "Previewed file '{}' as an image."
128 | msg = fmt.format(filename)
129 | logger.info(msg)
130 |
131 | @QtCore.pyqtSlot(QtGui.QStandardItem)
132 | def preview_item(self, item):
133 | self.clear()
134 |
135 | if not hasattr(item, 'key'):
136 | return
137 |
138 | try:
139 | name = item.name
140 | except AttributeError:
141 | name = None
142 |
143 | try:
144 | path = item.contentlocation
145 | except AttributeError:
146 | fmt = "Item '{}' doesn't have a contentlocation property."
147 | msg = fmt.format(repr(item))
148 | logger.critical(msg)
149 | return
150 |
151 | if not path:
152 | fmt = "Content for key '{}' is not available."
153 | msg = fmt.format(item.key)
154 | logger.error(msg)
155 | return
156 |
157 | mime, encoding = None, None
158 | if name:
159 | mime, encoding = mimetypes.guess_type(name)
160 | if not mime:
161 | mime, encoding = mimetypes.guess_type(path)
162 |
163 | if encoding:
164 | fmt = "Can't decode encoding '{}'."
165 | msg = fmt.format(encoding)
166 | logger.error(msg)
167 | return
168 |
169 | if not mime:
170 | if hasattr(item, 'name'):
171 | fmt = "Couldn't recognize mimetype for file '{}' ({})."
172 | msg = fmt.format(item.name, item.key)
173 | else:
174 | fmt = "Couldn't recognize mimetype for key '{}'."
175 | msg = fmt.format(item.key)
176 | logger.error(msg)
177 | return
178 |
179 | if mime.startswith('text/'):
180 | self.preview_text_file(path)
181 |
182 | elif mime.startswith('image/'):
183 | self.preview_image_file(path)
184 |
185 | else:
186 | fmt = "Can't preview mimetype '{}'."
187 | msg = fmt.format(mime)
188 | logger.error(msg)
189 | return
190 |
191 | def __repr__(self):
192 | return "{name}.{cls}({args})".format(
193 | name=__name__,
194 | cls=self.__class__.__name__,
195 | args='',
196 | )
197 |
198 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/key_metadata_model.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bisect
18 | import logging
19 |
20 | from PyQt5 import Qt
21 | from PyQt5 import QtCore
22 | from PyQt5 import QtGui
23 | from PyQt5 import QtWidgets
24 |
25 | from .utils import parse_as_set
26 | from .utils import AutoConsumed
27 |
28 | logger = logging.getLogger(__name__)
29 |
30 |
31 | class AnnexedKeyItem(QtGui.QStandardItem):
32 | def __init__(self, key_obj):
33 | super().__init__()
34 | self._obj = key_obj
35 |
36 | self.setText(self.key)
37 | self.setToolTip(self.key)
38 |
39 | font = QtGui.QFontDatabase.FixedFont
40 | font = QtGui.QFontDatabase().systemFont(font)
41 | self.setFont(font)
42 |
43 | icon = QtWidgets.QFileIconProvider.File
44 | icon = QtWidgets.QFileIconProvider().icon(icon)
45 | self.setIcon(icon)
46 |
47 | self.setSelectable(True)
48 | self.setEditable(False)
49 | self.setEnabled(True)
50 | self.setFlags(self.flags() | Qt.Qt.ItemNeverHasChildren)
51 |
52 | @property
53 | def metadata(self):
54 | return self._obj.metadata
55 |
56 | @property
57 | def key(self):
58 | return self._obj.key
59 |
60 | @property
61 | def contentlocation(self):
62 | return self._obj.contentlocation
63 |
64 | def type(self):
65 | return QtGui.QStandardItem.UserType + 1
66 |
67 | def __lt__(self, other):
68 | if other is None:
69 | return True
70 |
71 | elif isinstance(other, AnnexedKeyItem):
72 | lhs = self.data(role=Qt.Qt.DisplayRole)
73 | rhs = other.data(role=Qt.Qt.DisplayRole)
74 |
75 | lhs_pre, _, lhs_name = lhs.partition('--')
76 | lhs_backend, *lhs_fields = lhs_pre.split('-')
77 | lhs_fields = [(f[0], int(f[1:])) for f in lhs_fields]
78 | lhs = (lhs_backend, *lhs_fields, lhs_name)
79 |
80 | rhs_pre, _, rhs_name = rhs.partition('--')
81 | rhs_backend, *rhs_fields = rhs_pre.split('-')
82 | rhs_fields = [(f[0], int(f[1:])) for f in rhs_fields]
83 | rhs = (rhs_backend, *rhs_fields, rhs_name)
84 |
85 | try:
86 | return lhs > rhs
87 | except TypeError:
88 | return super().__lt__(other)
89 |
90 | else:
91 | return NotImplemented
92 |
93 | def __repr__(self):
94 | return "{name}.{cls}({args})".format(
95 | name=__name__,
96 | cls=self.__class__.__name__,
97 | args=self._obj.key,
98 | )
99 |
100 |
101 | class AnnexedFieldItem(QtGui.QStandardItem):
102 | def __init__(self, key_item, field):
103 | super().__init__()
104 | self._item = key_item
105 | self._field = field
106 |
107 | self.setSelectable(True)
108 | self.setEditable(True)
109 | self.setEnabled(True)
110 | self.setFlags(self.flags() | Qt.Qt.ItemNeverHasChildren)
111 |
112 | @property
113 | def key(self):
114 | return self._item.key
115 |
116 | @property
117 | def contentlocation(self):
118 | return self._item.contentlocation
119 |
120 | @property
121 | def metadata(self):
122 | return self._item.metadata.get(self._field, set())
123 |
124 | @metadata.setter
125 | def metadata(self, value):
126 | self._item.metadata[self._field] = value
127 | self.emitDataChanged()
128 |
129 | def type(self):
130 | return QtGui.QStandardItem.UserType + 2
131 |
132 | def data(self, role=Qt.Qt.DisplayRole):
133 | if role == Qt.Qt.DisplayRole:
134 | data = self.metadata
135 |
136 | if len(data) == 0:
137 | return None
138 | if len(data) == 1:
139 | return data.pop()
140 | else:
141 | return "<{n} values>".format(n=len(data))
142 |
143 | elif role == Qt.Qt.EditRole:
144 | data = self.metadata
145 | if data:
146 | return str(data)
147 |
148 | elif role == Qt.Qt.ToolTipRole:
149 | data = self.metadata
150 | if data:
151 | return str(data)
152 |
153 | elif role == Qt.Qt.UserRole:
154 | return self.metadata
155 |
156 | else:
157 | return super().data(role=role)
158 |
159 | def setData(self, value, role=Qt.Qt.EditRole):
160 | if role == Qt.Qt.DisplayRole:
161 | return False
162 |
163 | elif role == Qt.Qt.EditRole:
164 | try:
165 | self.metadata = parse_as_set(value)
166 | except:
167 | fmt = "Cannot parse '{}' as a set object."
168 | msg = fmt.format(value)
169 | logger.error(msg)
170 | return
171 |
172 | elif role == Qt.Qt.UserRole:
173 | try:
174 | self.metadata = value
175 | except:
176 | fmt = "Cannot parse '{}' as a set object."
177 | msg = fmt.format(value)
178 | logger.error(msg)
179 | return
180 |
181 | else:
182 | super().setData(value, role=role)
183 |
184 | def __lt__(self, other):
185 | if other is None:
186 | return True
187 |
188 | elif isinstance(other, AnnexedFieldItem):
189 | lhs = self.data(role=Qt.Qt.UserRole)
190 | rhs = other.data(role=Qt.Qt.UserRole)
191 | if len(lhs) == 0:
192 | return False
193 | elif len(rhs) == 0:
194 | return True
195 | elif len(lhs) == len(rhs) == 1:
196 | lhs_, rhs_ = lhs.pop(), rhs.pop()
197 | try:
198 | return int(lhs_) > int(rhs_)
199 | except ValueError:
200 | return super().__lt__(other)
201 | else:
202 | return len(lhs) > len(rhs)
203 |
204 | else:
205 | return NotImplemented
206 |
207 | def __repr__(self):
208 | return "{name}.{cls}({args})".format(
209 | name=__name__,
210 | cls=self.__class__.__name__,
211 | args={
212 | 'item': self._item,
213 | 'field': self._field,
214 | }
215 | )
216 |
217 |
218 | class AnnexedKeyMetadataModel(QtGui.QStandardItemModel):
219 | key_inserted = QtCore.pyqtSignal(str)
220 |
221 | def __init__(self, parent=None):
222 | super().__init__(parent)
223 | self.repo = None
224 |
225 | def setRepo(self, repo):
226 | if self._populate.running():
227 | self._populate.stop()
228 | msg = "Aborted loading previous key model."
229 | logger.info(msg)
230 |
231 | self.repo = repo
232 | self.fields = ['Git-Annex Key']
233 | self.key_items = {}
234 | self._pending = iter(self.repo.annex.values())
235 |
236 | self.clear()
237 | self.setHorizontalHeaderLabels(self.fields)
238 | self._populate.start()
239 |
240 | @QtCore.pyqtSlot()
241 | @AutoConsumed
242 | def _populate(self):
243 | msg = "Loading key model..."
244 | logger.info(msg)
245 |
246 | for obj in self._pending:
247 | self.insert_key(obj)
248 | yield
249 |
250 | msg = "Key model fully loaded."
251 | logger.info(msg)
252 |
253 | def insert_key(self, key_obj):
254 | key_item = AnnexedKeyItem(key_obj)
255 | field_items = (
256 | AnnexedFieldItem(key_obj, field)
257 | for field in self.fields[1:]
258 | )
259 | self.appendRow([key_item, *field_items])
260 | self.key_items[key_item.key] = key_item
261 |
262 | new_fields = set(key_item.metadata) - set(self.fields)
263 | for field in new_fields:
264 | QtCore.QMetaObject.invokeMethod(
265 | self, 'insert_field',
266 | Qt.Qt.QueuedConnection,
267 | QtCore.Q_ARG(str, field)
268 | )
269 |
270 | self.key_inserted.emit(key_obj.key)
271 |
272 | @QtCore.pyqtSlot(str)
273 | def insert_field(self, field):
274 | if field in self.fields:
275 | return
276 | col = bisect.bisect(self.fields, field, lo=1)
277 | items = [
278 | AnnexedFieldItem(self.item(row, 0), field)
279 | for row in range(self.rowCount())
280 | ]
281 | self.fields.insert(col, field)
282 | self.insertColumn(col, items)
283 | self.setHorizontalHeaderLabels(self.fields)
284 |
285 | def __repr__(self):
286 | return "{name}.{cls}({args})".format(
287 | name=__name__,
288 | cls=self.__class__.__name__,
289 | args=self.repo,
290 | )
291 |
292 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/main_window.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Git-Annex-Metadata-Gui
4 | # Copyright (C) 2017 Alper Nebi Yasak
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import bisect
20 | import functools
21 | import logging
22 |
23 | from PyQt5 import Qt
24 | from PyQt5 import QtCore
25 | from PyQt5 import QtGui
26 | from PyQt5 import QtWidgets
27 |
28 | from git_annex_adapter.repo import GitAnnexRepo
29 | from git_annex_adapter.exceptions import NotAGitAnnexRepoError
30 |
31 | from .key_metadata_model import AnnexedKeyMetadataModel
32 | from .file_metadata_model import AnnexedFileMetadataModel
33 | from .main_window_ui import Ui_MainWindow
34 | from .metadata_edit import MetadataEdit
35 |
36 | logger = logging.getLogger(__name__)
37 |
38 |
39 | class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
40 | def __init__(self, *args, **kwargs):
41 | super().__init__(*args, **kwargs)
42 | self.setupUi()
43 |
44 | self.repo = None
45 | self.model_keys = AnnexedKeyMetadataModel(self)
46 | self.view_keys.setModel(self.model_keys)
47 |
48 | self.model_head = AnnexedFileMetadataModel(self.view_head)
49 | self.model_head.setSourceModel(self.model_keys)
50 | self.view_head.setModel(self.model_head)
51 |
52 | def setupUi(self, window=None):
53 | if window is None:
54 | window = self
55 | super().setupUi(window)
56 |
57 | def retranslateUi(self, window=None):
58 | if window is None:
59 | window = self
60 | super().retranslateUi(window)
61 |
62 | @QtCore.pyqtSlot()
63 | @QtCore.pyqtSlot(str)
64 | def open_repo(self, path=None):
65 | if path is None:
66 | path = QtWidgets.QFileDialog.getExistingDirectory(self)
67 | if not path:
68 | logger.info('No path chosen to open.')
69 | return
70 |
71 | fmt = "Opening path '{}'."
72 | msg = fmt.format(path)
73 | logger.info(msg)
74 |
75 | try:
76 | self.repo = GitAnnexRepo(path)
77 | except NotAGitAnnexRepoError:
78 | fmt = "Path '{}' is not a git-annex repository."
79 | msg = fmt.format(path)
80 | logger.error(msg)
81 | else:
82 | self.refresh_repo()
83 |
84 | @QtCore.pyqtSlot()
85 | def refresh_repo(self):
86 | msg = "Refreshing key model, clearing preview and editor."
87 | logger.info(msg)
88 |
89 | if self.repo:
90 | self.model_keys.setRepo(self.repo)
91 | self.stack_preview.clear()
92 | self.metadata_edit.clear()
93 |
94 | @QtCore.pyqtSlot()
95 | def clear_header_menu(self):
96 | self.menu_headers.clear()
97 | self.menu_headers.setDisabled(True)
98 |
99 | @QtCore.pyqtSlot(str)
100 | def create_header_menu_action(self, header):
101 | actions = self.menu_headers.actions()
102 | headers = [act.text() for act in actions]
103 |
104 | if header in headers:
105 | return
106 | idx = bisect.bisect(headers, header)
107 |
108 | action = QtWidgets.QAction(self)
109 | action.setText(header)
110 | action.setCheckable(True)
111 | action.setChecked(True)
112 |
113 | def set_visibility(visible):
114 | self.view_keys.show_header(header, visible)
115 | self.view_head.show_header(header, visible)
116 |
117 | action.triggered.connect(set_visibility)
118 |
119 | def visibility_set(header_, visible):
120 | if header_ == header:
121 | action.setChecked(visible)
122 |
123 | for view in (self.view_keys, self.view_head):
124 | view.header_visibility_changed.connect(visibility_set)
125 |
126 | if idx < len(actions):
127 | before_action = actions[idx]
128 | self.menu_headers.insertAction(before_action, action)
129 | else:
130 | self.menu_headers.addAction(action)
131 |
132 | empty = len(self.menu_headers.actions()) == 0
133 | self.menu_headers.setDisabled(empty)
134 |
135 | @QtCore.pyqtSlot()
136 | def show_about_dialog(self):
137 | title = "About Git-Annex Metadata Gui"
138 | QtWidgets.QMessageBox.about(self, title, about_msg)
139 |
140 | def __repr__(self):
141 | return "{name}.{cls}({args})".format(
142 | name=__name__,
143 | cls=self.__class__.__name__,
144 | args='',
145 | )
146 |
147 |
148 | about_msg = """\
149 |
150 | Git-Annex Metadata Gui
151 | Version 0.2.0
152 |
153 |
154 |
155 | A graphical interface to the metadata functionality of \
156 | git-annex.
157 |
158 |
159 |
160 | Source code and bug tracking is available on \
161 | \
162 | GitHub.\
163 |
164 |
165 |
166 |
167 | Copyright (C) 2017 Alper Nebi Yasak \
168 | <\
169 | alpernebiyasak@gmail.com\
170 | >
171 |
172 |
173 |
174 | This program is free software: you can redistribute it and/or modify \
175 | it under the terms of the GNU General Public License as published by \
176 | the Free Software Foundation, either version 3 of the License, or \
177 | (at your option) any later version.
178 |
179 |
180 |
181 | This program is distributed in the hope that it will be useful, \
182 | but WITHOUT ANY WARRANTY; without even the implied warranty of \
183 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the \
184 | GNU General Public License for more details.
185 |
186 |
187 |
188 | You should have received a copy of the GNU General Public License \
189 | along with this program. If not, see \
190 | <\
191 | https://www.gnu.org/licenses/\
192 | >.\
193 |
194 | """
195 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/main_window_ui.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'qtdesigner-ui/main_window.ui'
4 | #
5 | # Created by: PyQt5 UI code generator 5.7
6 | #
7 | # WARNING! All changes made in this file will be lost!
8 |
9 | from PyQt5 import QtCore, QtGui, QtWidgets
10 |
11 | class Ui_MainWindow(object):
12 | def setupUi(self, MainWindow):
13 | MainWindow.setObjectName("MainWindow")
14 | MainWindow.resize(800, 600)
15 | MainWindow.setDockNestingEnabled(True)
16 | self.centralwidget = QtWidgets.QWidget(MainWindow)
17 | self.centralwidget.setObjectName("centralwidget")
18 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
19 | self.horizontalLayout.setObjectName("horizontalLayout")
20 | self.widget_tabs = QtWidgets.QTabWidget(self.centralwidget)
21 | self.widget_tabs.setObjectName("widget_tabs")
22 | self.tab_keys = QtWidgets.QWidget()
23 | self.tab_keys.setObjectName("tab_keys")
24 | self.gridLayout_2 = QtWidgets.QGridLayout(self.tab_keys)
25 | self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
26 | self.gridLayout_2.setObjectName("gridLayout_2")
27 | self.label_filter_keys = QtWidgets.QLabel(self.tab_keys)
28 | self.label_filter_keys.setObjectName("label_filter_keys")
29 | self.gridLayout_2.addWidget(self.label_filter_keys, 1, 0, 1, 1)
30 | self.edit_filter_keys = QtWidgets.QLineEdit(self.tab_keys)
31 | self.edit_filter_keys.setObjectName("edit_filter_keys")
32 | self.gridLayout_2.addWidget(self.edit_filter_keys, 1, 1, 1, 1)
33 | self.combo_filter_keys = QtWidgets.QComboBox(self.tab_keys)
34 | self.combo_filter_keys.setObjectName("combo_filter_keys")
35 | self.combo_filter_keys.addItem("")
36 | self.combo_filter_keys.addItem("")
37 | self.combo_filter_keys.addItem("")
38 | self.gridLayout_2.addWidget(self.combo_filter_keys, 1, 2, 1, 1)
39 | self.view_keys = MetadataTableView(self.tab_keys)
40 | self.view_keys.setAlternatingRowColors(True)
41 | self.view_keys.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
42 | self.view_keys.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
43 | self.view_keys.setShowGrid(False)
44 | self.view_keys.setSortingEnabled(True)
45 | self.view_keys.setWordWrap(False)
46 | self.view_keys.setCornerButtonEnabled(False)
47 | self.view_keys.setObjectName("view_keys")
48 | self.view_keys.horizontalHeader().setDefaultSectionSize(150)
49 | self.view_keys.verticalHeader().setDefaultSectionSize(20)
50 | self.view_keys.verticalHeader().setMinimumSectionSize(20)
51 | self.gridLayout_2.addWidget(self.view_keys, 0, 0, 1, 3)
52 | self.widget_tabs.addTab(self.tab_keys, "")
53 | self.tab_head = QtWidgets.QWidget()
54 | self.tab_head.setObjectName("tab_head")
55 | self.gridLayout_3 = QtWidgets.QGridLayout(self.tab_head)
56 | self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
57 | self.gridLayout_3.setObjectName("gridLayout_3")
58 | self.label_set_treeish = QtWidgets.QLabel(self.tab_head)
59 | self.label_set_treeish.setObjectName("label_set_treeish")
60 | self.gridLayout_3.addWidget(self.label_set_treeish, 1, 0, 1, 1)
61 | self.edit_set_treeish = QtWidgets.QLineEdit(self.tab_head)
62 | self.edit_set_treeish.setObjectName("edit_set_treeish")
63 | self.gridLayout_3.addWidget(self.edit_set_treeish, 1, 1, 1, 1)
64 | self.button_set_treeish = QtWidgets.QPushButton(self.tab_head)
65 | self.button_set_treeish.setObjectName("button_set_treeish")
66 | self.gridLayout_3.addWidget(self.button_set_treeish, 1, 2, 1, 1)
67 | self.view_head = MetadataTreeView(self.tab_head)
68 | self.view_head.setUniformRowHeights(True)
69 | self.view_head.setSortingEnabled(True)
70 | self.view_head.setObjectName("view_head")
71 | self.view_head.header().setDefaultSectionSize(150)
72 | self.view_head.header().setStretchLastSection(False)
73 | self.gridLayout_3.addWidget(self.view_head, 0, 0, 1, 3)
74 | self.widget_tabs.addTab(self.tab_head, "")
75 | self.horizontalLayout.addWidget(self.widget_tabs)
76 | MainWindow.setCentralWidget(self.centralwidget)
77 | self.menubar = QtWidgets.QMenuBar(MainWindow)
78 | self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 24))
79 | self.menubar.setObjectName("menubar")
80 | self.menu_file = QtWidgets.QMenu(self.menubar)
81 | self.menu_file.setObjectName("menu_file")
82 | self.menu_headers = QtWidgets.QMenu(self.menubar)
83 | self.menu_headers.setEnabled(False)
84 | self.menu_headers.setObjectName("menu_headers")
85 | self.menu_docks = QtWidgets.QMenu(self.menubar)
86 | self.menu_docks.setObjectName("menu_docks")
87 | self.menu_help = QtWidgets.QMenu(self.menubar)
88 | self.menu_help.setObjectName("menu_help")
89 | MainWindow.setMenuBar(self.menubar)
90 | self.statusbar = QtWidgets.QStatusBar(MainWindow)
91 | self.statusbar.setObjectName("statusbar")
92 | MainWindow.setStatusBar(self.statusbar)
93 | self.dock_preview = QtWidgets.QDockWidget(MainWindow)
94 | self.dock_preview.setObjectName("dock_preview")
95 | self.dock_preview_contents = QtWidgets.QWidget()
96 | self.dock_preview_contents.setObjectName("dock_preview_contents")
97 | self.gridLayout = QtWidgets.QGridLayout(self.dock_preview_contents)
98 | self.gridLayout.setContentsMargins(0, 0, 0, 0)
99 | self.gridLayout.setObjectName("gridLayout")
100 | self.stack_preview = FilePreview(self.dock_preview_contents)
101 | self.stack_preview.setObjectName("stack_preview")
102 | self.text_preview = QtWidgets.QPlainTextEdit()
103 | font = QtGui.QFont()
104 | font.setFamily("Monospace")
105 | self.text_preview.setFont(font)
106 | self.text_preview.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
107 | self.text_preview.setReadOnly(True)
108 | self.text_preview.setObjectName("text_preview")
109 | self.stack_preview.addWidget(self.text_preview)
110 | self.graphics_preview = QtWidgets.QGraphicsView()
111 | self.graphics_preview.setObjectName("graphics_preview")
112 | self.stack_preview.addWidget(self.graphics_preview)
113 | self.gridLayout.addWidget(self.stack_preview, 0, 0, 1, 1)
114 | self.dock_preview.setWidget(self.dock_preview_contents)
115 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.dock_preview)
116 | self.dock_metadata = QtWidgets.QDockWidget(MainWindow)
117 | self.dock_metadata.setObjectName("dock_metadata")
118 | self.dock_metadata_contents = QtWidgets.QWidget()
119 | self.dock_metadata_contents.setObjectName("dock_metadata_contents")
120 | self.gridLayout_4 = QtWidgets.QGridLayout(self.dock_metadata_contents)
121 | self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
122 | self.gridLayout_4.setObjectName("gridLayout_4")
123 | self.metadata_edit = MetadataEdit(self.dock_metadata_contents)
124 | self.metadata_edit.setObjectName("metadata_edit")
125 | self.gridLayout_4.addWidget(self.metadata_edit, 0, 0, 1, 1)
126 | self.dock_metadata.setWidget(self.dock_metadata_contents)
127 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.dock_metadata)
128 | self.action_open = QtWidgets.QAction(MainWindow)
129 | self.action_open.setObjectName("action_open")
130 | self.action_exit = QtWidgets.QAction(MainWindow)
131 | self.action_exit.setObjectName("action_exit")
132 | self.action_refresh = QtWidgets.QAction(MainWindow)
133 | self.action_refresh.setObjectName("action_refresh")
134 | self.action_dock_preview = QtWidgets.QAction(MainWindow)
135 | self.action_dock_preview.setCheckable(True)
136 | self.action_dock_preview.setChecked(True)
137 | self.action_dock_preview.setObjectName("action_dock_preview")
138 | self.action_dock_metadata = QtWidgets.QAction(MainWindow)
139 | self.action_dock_metadata.setCheckable(True)
140 | self.action_dock_metadata.setChecked(True)
141 | self.action_dock_metadata.setObjectName("action_dock_metadata")
142 | self.action_about = QtWidgets.QAction(MainWindow)
143 | self.action_about.setObjectName("action_about")
144 | self.menu_file.addAction(self.action_open)
145 | self.menu_file.addAction(self.action_refresh)
146 | self.menu_file.addAction(self.action_exit)
147 | self.menu_docks.addAction(self.action_dock_preview)
148 | self.menu_docks.addAction(self.action_dock_metadata)
149 | self.menu_help.addAction(self.action_about)
150 | self.menubar.addAction(self.menu_file.menuAction())
151 | self.menubar.addAction(self.menu_headers.menuAction())
152 | self.menubar.addAction(self.menu_docks.menuAction())
153 | self.menubar.addAction(self.menu_help.menuAction())
154 | self.label_filter_keys.setBuddy(self.edit_filter_keys)
155 | self.label_set_treeish.setBuddy(self.edit_set_treeish)
156 |
157 | self.retranslateUi(MainWindow)
158 | self.widget_tabs.setCurrentIndex(1)
159 | self.action_exit.triggered.connect(MainWindow.close)
160 | self.action_refresh.triggered.connect(MainWindow.refresh_repo)
161 | self.action_open.triggered.connect(MainWindow.open_repo)
162 | self.action_dock_metadata.triggered['bool'].connect(self.dock_metadata.setVisible)
163 | self.action_dock_preview.triggered['bool'].connect(self.dock_preview.setVisible)
164 | self.dock_metadata.visibilityChanged['bool'].connect(self.action_dock_metadata.setChecked)
165 | self.dock_preview.visibilityChanged['bool'].connect(self.action_dock_preview.setChecked)
166 | self.view_head.item_selected['QStandardItem'].connect(self.stack_preview.preview_item)
167 | self.view_keys.item_selected['QStandardItem'].connect(self.stack_preview.preview_item)
168 | self.view_head.item_selected['QStandardItem'].connect(self.metadata_edit.set_item)
169 | self.view_keys.item_selected['QStandardItem'].connect(self.metadata_edit.set_item)
170 | self.view_keys.header_created['QString'].connect(MainWindow.create_header_menu_action)
171 | self.view_head.header_created['QString'].connect(MainWindow.create_header_menu_action)
172 | self.view_head.model_reset.connect(MainWindow.clear_header_menu)
173 | self.view_keys.model_reset.connect(MainWindow.clear_header_menu)
174 | self.edit_filter_keys.textEdited['QString'].connect(self.view_keys.set_filter_pattern)
175 | self.combo_filter_keys.activated['QString'].connect(self.view_keys.set_filter_type)
176 | self.edit_set_treeish.textEdited['QString'].connect(self.view_head.set_treeish_to_build)
177 | self.edit_set_treeish.returnPressed.connect(self.view_head.rebuild_treeish)
178 | self.button_set_treeish.clicked.connect(self.view_head.rebuild_treeish)
179 | self.action_about.triggered.connect(MainWindow.show_about_dialog)
180 | QtCore.QMetaObject.connectSlotsByName(MainWindow)
181 |
182 | def retranslateUi(self, MainWindow):
183 | _translate = QtCore.QCoreApplication.translate
184 | MainWindow.setWindowTitle(_translate("MainWindow", "Git-Annex Metadata Gui"))
185 | self.label_filter_keys.setText(_translate("MainWindow", "Filter Keys:"))
186 | self.combo_filter_keys.setItemText(0, _translate("MainWindow", "Fixed"))
187 | self.combo_filter_keys.setItemText(1, _translate("MainWindow", "Regex"))
188 | self.combo_filter_keys.setItemText(2, _translate("MainWindow", "Wildcard"))
189 | self.widget_tabs.setTabText(self.widget_tabs.indexOf(self.tab_keys), _translate("MainWindow", "All Keys"))
190 | self.label_set_treeish.setText(_translate("MainWindow", "Set Treeish:"))
191 | self.edit_set_treeish.setText(_translate("MainWindow", "HEAD"))
192 | self.button_set_treeish.setText(_translate("MainWindow", "Build Treeish"))
193 | self.widget_tabs.setTabText(self.widget_tabs.indexOf(self.tab_head), _translate("MainWindow", "Work Tree"))
194 | self.menu_file.setTitle(_translate("MainWindow", "&File"))
195 | self.menu_headers.setTitle(_translate("MainWindow", "Headers"))
196 | self.menu_docks.setTitle(_translate("MainWindow", "Docks"))
197 | self.menu_help.setTitle(_translate("MainWindow", "Help"))
198 | self.dock_preview.setWindowTitle(_translate("MainWindow", "File Preview"))
199 | self.dock_metadata.setWindowTitle(_translate("MainWindow", "Metadata Editor"))
200 | self.action_open.setText(_translate("MainWindow", "&Open"))
201 | self.action_open.setShortcut(_translate("MainWindow", "Ctrl+O"))
202 | self.action_exit.setText(_translate("MainWindow", "E&xit"))
203 | self.action_exit.setShortcut(_translate("MainWindow", "Ctrl+Q"))
204 | self.action_refresh.setText(_translate("MainWindow", "&Refresh"))
205 | self.action_refresh.setShortcut(_translate("MainWindow", "F5"))
206 | self.action_dock_preview.setText(_translate("MainWindow", "File Preview"))
207 | self.action_dock_metadata.setText(_translate("MainWindow", "Metadata Editor"))
208 | self.action_about.setText(_translate("MainWindow", "About"))
209 |
210 | from git_annex_metadata_gui.file_preview import FilePreview
211 | from git_annex_metadata_gui.metadata_edit import MetadataEdit
212 | from git_annex_metadata_gui.metadata_table_view import MetadataTableView
213 | from git_annex_metadata_gui.metadata_tree_view import MetadataTreeView
214 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/metadata_edit.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import sip
18 | import logging
19 |
20 | from PyQt5 import Qt
21 | from PyQt5 import QtGui
22 | from PyQt5 import QtCore
23 | from PyQt5 import QtWidgets
24 |
25 | try:
26 | from .auto_size_line_edit import AutoSizeLineEdit
27 | from .field_item_edit import FieldItemEdit
28 | except ImportError:
29 | from auto_size_line_edit import AutoSizeLineEdit
30 | from field_item_edit import FieldItemEdit
31 |
32 | logger = logging.getLogger(__name__)
33 |
34 |
35 | class MetadataEdit(QtWidgets.QGroupBox):
36 | new_field_requested = QtCore.pyqtSignal(str)
37 |
38 | def __init__(self, parent=None):
39 | super().__init__(parent)
40 | self._item = None
41 | self._fields = []
42 | self._new_field_edit = None
43 | self.clear()
44 |
45 | @QtCore.pyqtSlot(QtGui.QStandardItem)
46 | def set_item(self, item):
47 | self.clear()
48 |
49 | if not self.isVisible():
50 | msg = "Metadata editor invisible, not setting file for it."
51 | logger.info(msg)
52 | return
53 |
54 | if not hasattr(item, 'key'):
55 | return
56 |
57 | self._item = item
58 |
59 | if hasattr(item, 'name'):
60 | desc = item.name
61 | else:
62 | desc = item.key
63 | self.setTitle(desc)
64 |
65 | model = self._item.model()
66 | model.columnsInserted.connect(self._on_columns_inserted)
67 | model.modelReset.connect(self.clear)
68 | self.new_field_requested.connect(model.insert_field)
69 |
70 | if self._new_field_edit is None:
71 | line_edit = AutoSizeLineEdit()
72 | line_edit.editingFinished.connect(self._request_new_field)
73 | line_edit.setPlaceholderText('+')
74 | line_edit.setAlignment(Qt.Qt.AlignCenter)
75 | self.layout().addRow(line_edit, QtWidgets.QWidget())
76 | self._new_field_edit = line_edit
77 |
78 | self.update_fields()
79 |
80 | fmt = "File '{}' set for metadata editing."
81 | msg = fmt.format(desc)
82 | logger.info(msg)
83 |
84 | @QtCore.pyqtSlot()
85 | def clear(self):
86 | try:
87 | model = self._item.model()
88 | except (AttributeError, RuntimeError):
89 | pass
90 | else:
91 | model.columnsInserted.disconnect(self._on_columns_inserted)
92 | self.new_field_requested.disconnect(model.insert_field)
93 |
94 | self._item = None
95 | self._fields = []
96 | self._new_field_edit = None
97 | self.setTitle('')
98 |
99 | if self.layout() is not None:
100 | while self.layout().count():
101 | item = self.layout().takeAt(0)
102 | if item:
103 | item.widget().deleteLater()
104 | sip.delete(self.layout())
105 |
106 | layout = QtWidgets.QFormLayout()
107 | layout.setFieldGrowthPolicy(layout.FieldsStayAtSizeHint)
108 | self.setLayout(layout)
109 |
110 | def update_fields(self):
111 | if self._item is None:
112 | return
113 |
114 | model = self._item.model()
115 | parent = self._item.parent()
116 | if not parent:
117 | parent = model.invisibleRootItem()
118 | row = self._item.row()
119 |
120 | for col, field in enumerate(model.fields[1:], 1):
121 | if field in self._fields:
122 | continue
123 | self._fields.append(field)
124 | field_item = parent.child(row, col)
125 | self.layout().insertRow(
126 | self.layout().rowCount() - 1,
127 | "{}: ".format(field),
128 | FieldItemEdit(field_item, parent=self),
129 | )
130 |
131 | def setTitle(self, title):
132 | if len(title) > 48:
133 | title = "{:.45}...".format(title)
134 | super().setTitle(title)
135 |
136 | def _request_new_field(self):
137 | field = self._new_field_edit.text()
138 | if field:
139 | fmt = "Requesting to create new metadata field '{}'."
140 | msg = fmt.format(field)
141 | logger.info(msg)
142 |
143 | self._new_field_edit.clear()
144 | self.new_field_requested.emit(field)
145 |
146 | def _on_columns_inserted(self, parent, first, last):
147 | if self._item is None:
148 | return
149 |
150 | parent_ = self._item.parent()
151 | if not parent_:
152 | parent_ = self._item.model().invisibleRootItem()
153 | parent_ = parent_.index()
154 |
155 | if parent == parent_:
156 | self.update_fields()
157 |
158 | def __repr__(self):
159 | return "{name}.{cls}({args})".format(
160 | name=__name__,
161 | cls=self.__class__.__name__,
162 | args=self._item,
163 | )
164 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/metadata_table_view.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import logging
18 |
19 | from PyQt5 import QtCore
20 | from PyQt5 import QtGui
21 | from PyQt5 import QtWidgets
22 |
23 | from .utils import StandardItemProxyModel
24 |
25 | logger = logging.getLogger(__name__)
26 |
27 |
28 | class MetadataTableView(QtWidgets.QTableView):
29 | item_selected = QtCore.pyqtSignal(QtGui.QStandardItem)
30 | header_visibility_changed = QtCore.pyqtSignal(str, bool)
31 | header_created = QtCore.pyqtSignal(str)
32 | model_reset = QtCore.pyqtSignal()
33 |
34 | def __init__(self, parent=None):
35 | super().__init__(parent)
36 | self._fields = []
37 | self._filter = ('', 'Fixed')
38 |
39 | def setModel(self, model):
40 | self._bare_model = model
41 | self._proxy_model = StandardItemProxyModel(model)
42 | self._proxy_model.setSourceModel(model)
43 | super().setModel(self._proxy_model)
44 |
45 | signal = self.selectionModel().selectionChanged
46 | signal.connect(self._on_selection_changed)
47 |
48 | signal = self._bare_model.headerDataChanged
49 | signal.connect(self._on_header_data_changed)
50 |
51 | signal = self._bare_model.modelReset
52 | signal.connect(self._on_model_reset)
53 |
54 | @QtCore.pyqtSlot(str)
55 | @QtCore.pyqtSlot(str, bool)
56 | def show_header(self, title, visible=True):
57 | if title not in self._bare_model.fields:
58 | return
59 | idx = self._bare_model.fields.index(title)
60 | header = self.horizontalHeader()
61 | if header.isSectionHidden(idx) != (not visible):
62 | header.setSectionHidden(idx, not visible)
63 | self.header_visibility_changed.emit(title, visible)
64 |
65 | fmt = "{} table column for field '{}'."
66 | msg = fmt.format('Showing' if visible else 'Hiding', title)
67 | logger.info(msg)
68 |
69 | @QtCore.pyqtSlot(str)
70 | def hide_header(self, title):
71 | self.show_header(title, False)
72 |
73 | @QtCore.pyqtSlot(str)
74 | def create_header(self, title):
75 | self._bare_model.insert_field(title)
76 |
77 | @QtCore.pyqtSlot(str)
78 | def set_filter_pattern(self, filter_pattern):
79 | self._filter = (filter_pattern, self._filter[1])
80 | self.filter()
81 |
82 | @QtCore.pyqtSlot(str)
83 | def set_filter_type(self, filter_type):
84 | self._filter = (self._filter[0], filter_type)
85 | self.filter()
86 |
87 | def filter(self):
88 | pattern, type_ = self._filter
89 |
90 | if not self.model():
91 | return
92 |
93 | if type_ == 'Fixed':
94 | self.model().setFilterFixedString(pattern)
95 | elif type_ == 'Regex':
96 | self.model().setFilterRegExp(pattern)
97 | elif type_ == 'Wildcard':
98 | self.model().setFilterWildcard(pattern)
99 |
100 | if pattern:
101 | fmt = "Filtered keys with {} pattern '{}'."
102 | msg = fmt.format(type_, pattern)
103 | else:
104 | msg = "Removed key filter."
105 | logger.info(msg)
106 |
107 | def _on_selection_changed(self, selected, deselected):
108 | indexes = selected.indexes()
109 | if not indexes:
110 | return
111 |
112 | index = indexes[0]
113 | src_index = index.model().mapToSource(index)
114 | item = src_index.model().itemFromIndex(src_index)
115 |
116 | self.item_selected.emit(item)
117 |
118 | def _on_header_data_changed(self, orientation, first, last):
119 | fields = self._bare_model.fields[1:]
120 |
121 | for field in fields:
122 | if field not in self._fields:
123 | self.header_created.emit(field)
124 |
125 | self._fields = fields
126 |
127 | def _on_model_reset(self):
128 | self._fields = []
129 | self.model_reset.emit()
130 |
131 | def __repr__(self):
132 | return "{name}.{cls}({args})".format(
133 | name=__name__,
134 | cls=self.__class__.__name__,
135 | args='',
136 | )
137 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/metadata_tree_view.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import logging
18 |
19 | from PyQt5 import Qt
20 | from PyQt5 import QtCore
21 | from PyQt5 import QtGui
22 | from PyQt5 import QtWidgets
23 |
24 | logger = logging.getLogger(__name__)
25 |
26 | from .utils import StandardItemProxyModel
27 |
28 | class MetadataTreeView(QtWidgets.QTreeView):
29 | item_selected = QtCore.pyqtSignal(QtGui.QStandardItem)
30 | header_visibility_changed = QtCore.pyqtSignal(str, bool)
31 | header_created = QtCore.pyqtSignal(str)
32 | model_reset = QtCore.pyqtSignal()
33 |
34 | def __init__(self, parent=None):
35 | super().__init__(parent)
36 | self._treeish = 'HEAD'
37 | self.sortByColumn(0, Qt.Qt.AscendingOrder)
38 |
39 | def setModel(self, model):
40 | self._bare_model = model
41 | self._proxy_model = StandardItemProxyModel(model)
42 | self._proxy_model.setSourceModel(model)
43 | super().setModel(self._proxy_model)
44 |
45 | signal = self.selectionModel().selectionChanged
46 | signal.connect(self._on_selection_changed)
47 |
48 | signal = self._bare_model.headerDataChanged
49 | signal.connect(self._on_header_data_changed)
50 |
51 | signal = self._bare_model.modelReset
52 | signal.connect(self._on_model_reset)
53 |
54 | @QtCore.pyqtSlot(str)
55 | @QtCore.pyqtSlot(str, bool)
56 | def show_header(self, title, visible=True):
57 | if title not in self._bare_model.fields:
58 | return
59 | idx = self._bare_model.fields.index(title)
60 | header = self.header()
61 | if header.isSectionHidden(idx) != (not visible):
62 | header.setSectionHidden(idx, not visible)
63 | self.header_visibility_changed.emit(title, visible)
64 |
65 | fmt = "{} tree column for field '{}'."
66 | msg = fmt.format('Showing' if visible else 'Hiding', title)
67 | logger.info(msg)
68 |
69 | @QtCore.pyqtSlot(str)
70 | def hide_header(self, title):
71 | self.show_header(title, False)
72 |
73 | @QtCore.pyqtSlot(str)
74 | def create_header(self, title):
75 | self._bare_model.insert_field(title)
76 |
77 | @QtCore.pyqtSlot(str)
78 | def set_treeish_to_build(self, treeish):
79 | if treeish:
80 | self._treeish = treeish
81 | else:
82 | self._treeish = 'HEAD'
83 |
84 | @QtCore.pyqtSlot()
85 | def rebuild_treeish(self):
86 | if not self.model():
87 | return
88 |
89 | fmt = "Building tree for git treeish '{}'."
90 | msg = fmt.format(self._treeish)
91 | logger.info(msg)
92 |
93 | self._bare_model.setTreeish(self._treeish)
94 |
95 | def _on_selection_changed(self, selected, deselected):
96 | indexes = selected.indexes()
97 | if not indexes:
98 | return
99 |
100 | index = indexes[0]
101 | src_index = index.model().mapToSource(index)
102 | item = src_index.model().itemFromIndex(src_index)
103 |
104 | self.item_selected.emit(item)
105 |
106 | def _on_header_data_changed(self, orientation, first, last):
107 | fields = self._bare_model.fields[1:]
108 |
109 | for field in fields:
110 | if field not in self._fields:
111 | self.header_created.emit(field)
112 |
113 | self._fields = fields
114 |
115 | def _on_model_reset(self):
116 | self._fields = []
117 | self.model_reset.emit()
118 |
119 | def __repr__(self):
120 | return "{name}.{cls}({args})".format(
121 | name=__name__,
122 | cls=self.__class__.__name__,
123 | args='',
124 | )
125 |
--------------------------------------------------------------------------------
/git_annex_metadata_gui/utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Git-Annex-Metadata-Gui
4 | # Copyright (C) 2017 Alper Nebi Yasak
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import ast
20 | import functools
21 | import logging
22 | import time
23 |
24 | from PyQt5 import Qt
25 | from PyQt5 import QtGui
26 | from PyQt5 import QtCore
27 |
28 | logger = logging.getLogger(__name__)
29 |
30 |
31 | def parse_as_set(x):
32 | if x == '{}':
33 | return set()
34 |
35 | try:
36 | xs = ast.literal_eval(x)
37 | assert isinstance(xs, set)
38 | return xs
39 |
40 | except Exception as err:
41 | fmt = "Can't interpret '{}' as a set."
42 | msg = fmt.format(x)
43 | raise ValueError(msg) from err
44 |
45 |
46 | class AutoConsumed:
47 | _timeout = 0.05
48 |
49 | def __init__(self, function):
50 | self._function = function
51 | self._generator = None
52 | self._instance = None
53 | functools.update_wrapper(self, function)
54 |
55 | def start(self, *args):
56 | self._generator = self._function(self._instance, *args)
57 | self()
58 |
59 | def running(self):
60 | return self._generator is not None
61 |
62 | def stop(self):
63 | self._generator = None
64 |
65 | def __call__(self, instance=None):
66 | if instance is not None and instance is not self._instance:
67 | fmt = "Instance mismatch on autoconsumer {}, ({} vs {})."
68 | msg = fmt.format(
69 | self._function.__name__,
70 | instance, self._instance,
71 | )
72 | logger.critical(msg)
73 |
74 | if self._generator is None:
75 | return
76 |
77 | try:
78 | endtime = time.monotonic() + self._timeout
79 | while time.monotonic() < endtime:
80 | next(self._generator)
81 |
82 | except StopIteration:
83 | self._generator = None
84 |
85 | else:
86 | QtCore.QMetaObject.invokeMethod(
87 | self._instance, self._function.__name__,
88 | Qt.Qt.QueuedConnection,
89 | )
90 |
91 | def __get__(self, instance, owner):
92 | self._instance = instance
93 | return self
94 |
95 | def __repr__(self):
96 | return "{name}.{cls}({args})".format(
97 | name=__name__,
98 | cls=self.__class__.__name__,
99 | args=self._function.__name__,
100 | )
101 |
102 |
103 | class DataProxyItem(QtGui.QStandardItem):
104 | def __init__(self, item):
105 | super().__init__()
106 | self._item = item
107 |
108 | model = self._item.model()
109 | model.dataChanged.connect(self._propagate_changes)
110 |
111 | def type(self):
112 | return QtGui.QStandardItem.UserType + 3
113 |
114 | def data(self, role=Qt.Qt.DisplayRole):
115 | return self._item.data(role=role)
116 |
117 | def setData(self, value, role=Qt.Qt.EditRole):
118 | return self._item.setData(value, role=role)
119 |
120 | def flags(self):
121 | return self._item.flags()
122 |
123 | def _propagate_changes(self, topLeft, bottomRight, roles):
124 | rows = range(topLeft.row(), bottomRight.row() + 1)
125 | columns = range(topLeft.column(), bottomRight.column() + 1)
126 |
127 | if self._item.row() in rows and self._item.column() in columns:
128 | self.emitDataChanged()
129 |
130 | def __repr__(self):
131 | return "{name}.{cls}({args})".format(
132 | name=__name__,
133 | cls=self.__class__.__name__,
134 | args=self._item,
135 | )
136 |
137 |
138 | class StatusBarLogHandler(logging.Handler):
139 | def __init__(self, statusbar):
140 | super().__init__()
141 | self._statusbar = statusbar
142 |
143 | def emit(self, record):
144 | msg = self.format(record)
145 | line = msg.split('\n')[0]
146 | self._statusbar.showMessage(line, msecs=5000)
147 |
148 | def __repr__(self):
149 | return "{name}.{cls}({args})".format(
150 | name=__name__,
151 | cls=self.__class__.__name__,
152 | args=self._statusbar,
153 | )
154 |
155 |
156 | class StandardItemProxyModel(QtCore.QSortFilterProxyModel):
157 | def lessThan(self, source_left, source_right):
158 | descending = (self.sortOrder() == Qt.Qt.DescendingOrder)
159 |
160 | lhs_flags = source_left.sibling(source_left.row(), 0).flags()
161 | lhs_is_dir = not bool(lhs_flags & Qt.Qt.ItemNeverHasChildren)
162 |
163 | rhs_flags = source_right.sibling(source_right.row(), 0).flags()
164 | rhs_is_dir = not bool(rhs_flags & Qt.Qt.ItemNeverHasChildren)
165 |
166 | if lhs_is_dir and (not rhs_is_dir):
167 | return not descending
168 | elif (not lhs_is_dir) and rhs_is_dir:
169 | return descending
170 |
171 | model = self.sourceModel()
172 | lhs = model.itemFromIndex(source_left)
173 | rhs = model.itemFromIndex(source_right)
174 |
175 | try:
176 | return (lhs < rhs)
177 | except TypeError:
178 | return super().lessThan(source_left, source_right)
179 |
180 |
--------------------------------------------------------------------------------
/qtdesigner-plugins/auto_size_line_edit_plugin.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from PyQt5 import QtCore
18 | from PyQt5 import QtGui
19 | from PyQt5 import QtWidgets
20 | from PyQt5 import QtDesigner
21 |
22 | from auto_size_line_edit import AutoSizeLineEdit
23 |
24 | class AutoSizeLineEditPlugin(QtDesigner.QPyDesignerCustomWidgetPlugin):
25 | def __init__(self, parent=None):
26 | super().__init__(parent)
27 | self._initialized = False
28 |
29 | def initialize(self, formEditor):
30 | self._initialized = True
31 |
32 | def isInitialized(self):
33 | return self._initialized
34 |
35 | def createWidget(self, parent):
36 | return AutoSizeLineEdit(parent)
37 |
38 | def name(self):
39 | return "AutoSizeLineEdit"
40 |
41 | def group(self):
42 | return "Git-Annex Metadata Gui Widgets"
43 |
44 | def icon(self):
45 | return QtGui.QIcon()
46 |
47 | def toolTip(self):
48 | return ""
49 |
50 | def whatsThis(self):
51 | return ""
52 |
53 | def isContainer(self):
54 | return False
55 |
56 | def includeFile(self):
57 | return "git_annex_metadata_gui.auto_size_line_edit"
58 |
59 | def domXml(self):
60 | cls = self.name()
61 | name = self.name()
62 | xml = ''
63 | return xml.format(cls, name)
64 |
65 |
--------------------------------------------------------------------------------
/qtdesigner-plugins/field_item_edit_plugin.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from PyQt5 import Qt
18 | from PyQt5 import QtCore
19 | from PyQt5 import QtGui
20 | from PyQt5 import QtWidgets
21 | from PyQt5 import QtDesigner
22 |
23 | from field_item_edit import FieldItemEdit
24 |
25 | class FieldItemEditPlugin(QtDesigner.QPyDesignerCustomWidgetPlugin):
26 | def __init__(self, parent=None):
27 | super().__init__(parent)
28 | self._initialized = False
29 |
30 | def initialize(self, formEditor):
31 | self._initialized = True
32 |
33 | def isInitialized(self):
34 | return self._initialized
35 |
36 | def createWidget(self, parent):
37 | item = MockFieldItem()
38 | return FieldItemEdit(item, parent)
39 |
40 | def name(self):
41 | return "FieldItemEdit"
42 |
43 | def group(self):
44 | return "Git-Annex Metadata Gui Widgets"
45 |
46 | def icon(self):
47 | return QtGui.QIcon()
48 |
49 | def toolTip(self):
50 | return ""
51 |
52 | def whatsThis(self):
53 | return ""
54 |
55 | def isContainer(self):
56 | return False
57 |
58 | def includeFile(self):
59 | return "git_annex_metadata_gui.field_item_edit"
60 |
61 | def domXml(self):
62 | cls = self.name()
63 | name = self.name()
64 | xml = ''
65 | return xml.format(cls, name)
66 |
67 |
68 | class MockFieldItem(QtGui.QStandardItem):
69 | def __init__(self):
70 | super().__init__()
71 | self._set = {'foo', 'bar'}
72 |
73 | self._model = QtGui.QStandardItemModel()
74 | self._model.appendRow(self)
75 |
76 | def data(self, role=Qt.Qt.DisplayRole):
77 | if role == Qt.Qt.UserRole:
78 | return self._set
79 | else:
80 | return super().data(role=role)
81 |
82 | def setData(self, value, role=Qt.Qt.DisplayRole):
83 | if role == Qt.Qt.UserRole:
84 | self._set = set(value)
85 | self.emitDataChanged()
86 | else:
87 | super().setData(value, role=role)
88 |
89 |
--------------------------------------------------------------------------------
/qtdesigner-plugins/file_preview_plugin.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from PyQt5 import QtCore
18 | from PyQt5 import QtGui
19 | from PyQt5 import QtWidgets
20 | from PyQt5 import QtDesigner
21 |
22 | from file_preview import FilePreview
23 |
24 | class FilePreviewPlugin(QtDesigner.QPyDesignerCustomWidgetPlugin):
25 | def __init__(self, parent=None):
26 | super().__init__(parent)
27 | self._initialized = False
28 |
29 | def initialize(self, formEditor):
30 | self._initialized = True
31 |
32 | def isInitialized(self):
33 | return self._initialized
34 |
35 | def createWidget(self, parent):
36 | return FilePreview(parent)
37 |
38 | def name(self):
39 | return "FilePreview"
40 |
41 | def group(self):
42 | return "Git-Annex Metadata Gui Widgets"
43 |
44 | def icon(self):
45 | return QtGui.QIcon()
46 |
47 | def toolTip(self):
48 | return ""
49 |
50 | def whatsThis(self):
51 | return ""
52 |
53 | def isContainer(self):
54 | return False
55 |
56 | def includeFile(self):
57 | return "git_annex_metadata_gui.file_preview"
58 |
59 | def domXml(self):
60 | xml = """\
61 |
62 |
63 |
64 |
65 | """
66 | return xml
67 |
68 |
--------------------------------------------------------------------------------
/qtdesigner-plugins/metadata_edit_plugin.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bisect
18 |
19 | from PyQt5 import Qt
20 | from PyQt5 import QtCore
21 | from PyQt5 import QtGui
22 | from PyQt5 import QtWidgets
23 | from PyQt5 import QtDesigner
24 |
25 | from metadata_edit import MetadataEdit
26 |
27 | class MetadataEditPlugin(QtDesigner.QPyDesignerCustomWidgetPlugin):
28 | def __init__(self, parent=None):
29 | super().__init__(parent)
30 | self._models = []
31 | self._initialized = False
32 |
33 | def initialize(self, formEditor):
34 | self._initialized = True
35 |
36 | def isInitialized(self):
37 | return self._initialized
38 |
39 | def createWidget(self, parent):
40 | model = MockMetadataModel()
41 | self._models.append(model)
42 | key_item = model.item(0, 0)
43 | widget = MetadataEdit(parent)
44 | widget.isVisible = lambda : True
45 | widget.set_item(key_item)
46 | return widget
47 |
48 | def name(self):
49 | return "MetadataEdit"
50 |
51 | def group(self):
52 | return "Git-Annex Metadata Gui Widgets"
53 |
54 | def icon(self):
55 | return QtGui.QIcon()
56 |
57 | def toolTip(self):
58 | return ""
59 |
60 | def whatsThis(self):
61 | return ""
62 |
63 | def isContainer(self):
64 | return False
65 |
66 | def includeFile(self):
67 | return "git_annex_metadata_gui.metadata_edit"
68 |
69 | def domXml(self):
70 | cls = self.name()
71 | name = self.name()
72 | xml = ''
73 | return xml.format(cls, name)
74 |
75 |
76 | class MockMetadataModel(QtGui.QStandardItemModel):
77 | def __init__(self, parent=None):
78 | super().__init__(parent)
79 | item = MockKeyItem('SHA256E-s0--0')
80 | self.appendRow(item)
81 |
82 | self.fields = ['Git-Annex Key']
83 | self.setHorizontalHeaderLabels(self.fields)
84 |
85 | for field in ['baz', 'diz']:
86 | self.insert_field(field)
87 |
88 | @QtCore.pyqtSlot(str)
89 | def insert_field(self, field):
90 | if field in self.fields:
91 | return
92 | col = bisect.bisect(self.fields, field, lo=1)
93 | self.fields.insert(col, field)
94 | self.insertColumn(col, [MockFieldItem()])
95 | self.setHorizontalHeaderLabels(self.fields)
96 |
97 |
98 | class MockKeyItem(QtGui.QStandardItem):
99 | def __init__(self, text):
100 | super().__init__(text)
101 |
102 | @property
103 | def key(self):
104 | return self.text()
105 |
106 |
107 | class MockFieldItem(QtGui.QStandardItem):
108 | def __init__(self):
109 | super().__init__()
110 | self._set = {'foo', 'bar'}
111 |
112 | def data(self, role=Qt.Qt.DisplayRole):
113 | if role == Qt.Qt.UserRole:
114 | return self._set
115 | else:
116 | return super().data(role=role)
117 |
118 | def setData(self, value, role=Qt.Qt.DisplayRole):
119 | if role == Qt.Qt.UserRole:
120 | self._set = set(value)
121 | self.emitDataChanged()
122 | else:
123 | super().setData(value, role=role)
124 |
125 |
--------------------------------------------------------------------------------
/qtdesigner-plugins/metadata_table_view_plugin.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from PyQt5 import QtCore
18 | from PyQt5 import QtGui
19 | from PyQt5 import QtWidgets
20 | from PyQt5 import QtDesigner
21 |
22 | from metadata_table_view import MetadataTableView
23 |
24 | class MetadataTableViewPlugin(QtDesigner.QPyDesignerCustomWidgetPlugin):
25 | def __init__(self, parent=None):
26 | super().__init__(parent)
27 | self._initialized = False
28 |
29 | def initialize(self, formEditor):
30 | self._initialized = True
31 |
32 | def isInitialized(self):
33 | return self._initialized
34 |
35 | def createWidget(self, parent):
36 | return MetadataTableView(parent)
37 |
38 | def name(self):
39 | return "MetadataTableView"
40 |
41 | def group(self):
42 | return "Git-Annex Metadata Gui Widgets"
43 |
44 | def icon(self):
45 | return QtGui.QIcon()
46 |
47 | def toolTip(self):
48 | return ""
49 |
50 | def whatsThis(self):
51 | return ""
52 |
53 | def isContainer(self):
54 | return False
55 |
56 | def includeFile(self):
57 | return "git_annex_metadata_gui.metadata_table_view"
58 |
59 | def domXml(self):
60 | cls = self.name()
61 | name = 'view_metadata_table'
62 | xml = ''
63 | return xml.format(cls, name)
64 |
65 |
--------------------------------------------------------------------------------
/qtdesigner-plugins/metadata_tree_view_plugin.py:
--------------------------------------------------------------------------------
1 | # Git-Annex-Metadata-Gui
2 | # Copyright (C) 2017 Alper Nebi Yasak
3 | #
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from PyQt5 import QtCore
18 | from PyQt5 import QtGui
19 | from PyQt5 import QtWidgets
20 | from PyQt5 import QtDesigner
21 |
22 | from metadata_tree_view import MetadataTreeView
23 |
24 | class MetadataTreeViewPlugin(QtDesigner.QPyDesignerCustomWidgetPlugin):
25 | def __init__(self, parent=None):
26 | super().__init__(parent)
27 | self._initialized = False
28 |
29 | def initialize(self, formEditor):
30 | self._initialized = True
31 |
32 | def isInitialized(self):
33 | return self._initialized
34 |
35 | def createWidget(self, parent):
36 | return MetadataTreeView(parent)
37 |
38 | def name(self):
39 | return "MetadataTreeView"
40 |
41 | def group(self):
42 | return "Git-Annex Metadata Gui Widgets"
43 |
44 | def icon(self):
45 | return QtGui.QIcon()
46 |
47 | def toolTip(self):
48 | return ""
49 |
50 | def whatsThis(self):
51 | return ""
52 |
53 | def isContainer(self):
54 | return False
55 |
56 | def includeFile(self):
57 | return "git_annex_metadata_gui.metadata_tree_view"
58 |
59 | def domXml(self):
60 | cls = self.name()
61 | name = 'view_metadata_tree'
62 | xml = ''
63 | return xml.format(cls, name)
64 |
65 |
--------------------------------------------------------------------------------
/qtdesigner-ui/main_window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 |
7 | 0
8 | 0
9 | 800
10 | 600
11 |
12 |
13 |
14 | Git-Annex Metadata Gui
15 |
16 |
17 | true
18 |
19 |
20 |
21 | -
22 |
23 |
24 | 1
25 |
26 |
27 |
28 | All Keys
29 |
30 |
31 |
-
32 |
33 |
34 | Filter Keys:
35 |
36 |
37 | edit_filter_keys
38 |
39 |
40 |
41 | -
42 |
43 |
44 | -
45 |
46 |
-
47 |
48 | Fixed
49 |
50 |
51 | -
52 |
53 | Regex
54 |
55 |
56 | -
57 |
58 | Wildcard
59 |
60 |
61 |
62 |
63 | -
64 |
65 |
66 | true
67 |
68 |
69 | QAbstractItemView::SingleSelection
70 |
71 |
72 | QAbstractItemView::ScrollPerPixel
73 |
74 |
75 | false
76 |
77 |
78 | true
79 |
80 |
81 | false
82 |
83 |
84 | false
85 |
86 |
87 | 150
88 |
89 |
90 | 20
91 |
92 |
93 | 20
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Work Tree
102 |
103 |
104 | -
105 |
106 |
107 | Set Treeish:
108 |
109 |
110 | edit_set_treeish
111 |
112 |
113 |
114 | -
115 |
116 |
117 | HEAD
118 |
119 |
120 |
121 | -
122 |
123 |
124 | Build Treeish
125 |
126 |
127 |
128 | -
129 |
130 |
131 | true
132 |
133 |
134 | true
135 |
136 |
137 | 150
138 |
139 |
140 | false
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
193 |
194 |
195 |
196 | File Preview
197 |
198 |
199 | 2
200 |
201 |
202 |
203 | -
204 |
205 |
206 |
207 |
208 | Monospace
209 |
210 |
211 |
212 | QPlainTextEdit::NoWrap
213 |
214 |
215 | true
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | Metadata Editor
227 |
228 |
229 | 2
230 |
231 |
232 |
233 | -
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 | &Open
242 |
243 |
244 | Ctrl+O
245 |
246 |
247 |
248 |
249 | E&xit
250 |
251 |
252 | Ctrl+Q
253 |
254 |
255 |
256 |
257 | &Refresh
258 |
259 |
260 | F5
261 |
262 |
263 |
264 |
265 | true
266 |
267 |
268 | true
269 |
270 |
271 | File Preview
272 |
273 |
274 |
275 |
276 | true
277 |
278 |
279 | true
280 |
281 |
282 | Metadata Editor
283 |
284 |
285 |
286 |
287 | About
288 |
289 |
290 |
291 |
292 |
293 | FilePreview
294 | QStackedWidget
295 | git_annex_metadata_gui.file_preview
296 |
297 |
298 | MetadataEdit
299 | QGroupBox
300 | git_annex_metadata_gui.metadata_edit
301 |
302 |
303 | MetadataTableView
304 | QTableView
305 | git_annex_metadata_gui.metadata_table_view
306 |
307 |
308 | MetadataTreeView
309 | QTreeView
310 | git_annex_metadata_gui.metadata_tree_view
311 |
312 |
313 |
314 |
315 |
316 | action_exit
317 | triggered()
318 | MainWindow
319 | close()
320 |
321 |
322 | -1
323 | -1
324 |
325 |
326 | 399
327 | 299
328 |
329 |
330 |
331 |
332 | action_refresh
333 | triggered()
334 | MainWindow
335 | refresh_repo()
336 |
337 |
338 | -1
339 | -1
340 |
341 |
342 | 399
343 | 299
344 |
345 |
346 |
347 |
348 | action_open
349 | triggered()
350 | MainWindow
351 | open_repo()
352 |
353 |
354 | -1
355 | -1
356 |
357 |
358 | 399
359 | 299
360 |
361 |
362 |
363 |
364 | action_dock_metadata
365 | triggered(bool)
366 | dock_metadata
367 | setVisible(bool)
368 |
369 |
370 | -1
371 | -1
372 |
373 |
374 | 662
375 | 457
376 |
377 |
378 |
379 |
380 | action_dock_preview
381 | triggered(bool)
382 | dock_preview
383 | setVisible(bool)
384 |
385 |
386 | -1
387 | -1
388 |
389 |
390 | 662
391 | 178
392 |
393 |
394 |
395 |
396 | dock_metadata
397 | visibilityChanged(bool)
398 | action_dock_metadata
399 | setChecked(bool)
400 |
401 |
402 | 662
403 | 457
404 |
405 |
406 | -1
407 | -1
408 |
409 |
410 |
411 |
412 | dock_preview
413 | visibilityChanged(bool)
414 | action_dock_preview
415 | setChecked(bool)
416 |
417 |
418 | 662
419 | 178
420 |
421 |
422 | -1
423 | -1
424 |
425 |
426 |
427 |
428 | view_head
429 | item_selected(QStandardItem)
430 | stack_preview
431 | preview_item(QStandardItem)
432 |
433 |
434 | 101
435 | 76
436 |
437 |
438 | 662
439 | 76
440 |
441 |
442 |
443 |
444 | view_keys
445 | item_selected(QStandardItem)
446 | stack_preview
447 | preview_item(QStandardItem)
448 |
449 |
450 | 101
451 | 76
452 |
453 |
454 | 662
455 | 190
456 |
457 |
458 |
459 |
460 | view_head
461 | item_selected(QStandardItem)
462 | metadata_edit
463 | set_item(QStandardItem)
464 |
465 |
466 | 101
467 | 76
468 |
469 |
470 | 662
471 | 470
472 |
473 |
474 |
475 |
476 | view_keys
477 | item_selected(QStandardItem)
478 | metadata_edit
479 | set_item(QStandardItem)
480 |
481 |
482 | 101
483 | 76
484 |
485 |
486 | 662
487 | 470
488 |
489 |
490 |
491 |
492 | view_keys
493 | header_created(QString)
494 | MainWindow
495 | create_header_menu_action(QString)
496 |
497 |
498 | 101
499 | 76
500 |
501 |
502 | 399
503 | 299
504 |
505 |
506 |
507 |
508 | view_head
509 | header_created(QString)
510 | MainWindow
511 | create_header_menu_action(QString)
512 |
513 |
514 | 101
515 | 76
516 |
517 |
518 | 399
519 | 299
520 |
521 |
522 |
523 |
524 | view_head
525 | model_reset()
526 | MainWindow
527 | clear_header_menu()
528 |
529 |
530 | 101
531 | 76
532 |
533 |
534 | 399
535 | 299
536 |
537 |
538 |
539 |
540 | view_keys
541 | model_reset()
542 | MainWindow
543 | clear_header_menu()
544 |
545 |
546 | 101
547 | 76
548 |
549 |
550 | 399
551 | 299
552 |
553 |
554 |
555 |
556 | edit_filter_keys
557 | textEdited(QString)
558 | view_keys
559 | set_filter_pattern(QString)
560 |
561 |
562 | 72
563 | 85
564 |
565 |
566 | 101
567 | 76
568 |
569 |
570 |
571 |
572 | combo_filter_keys
573 | activated(QString)
574 | view_keys
575 | set_filter_type(QString)
576 |
577 |
578 | 101
579 | 85
580 |
581 |
582 | 101
583 | 76
584 |
585 |
586 |
587 |
588 | edit_set_treeish
589 | textEdited(QString)
590 | view_head
591 | set_treeish_to_build(QString)
592 |
593 |
594 | 168
595 | 538
596 |
597 |
598 | 168
599 | 508
600 |
601 |
602 |
603 |
604 | edit_set_treeish
605 | returnPressed()
606 | view_head
607 | rebuild_treeish()
608 |
609 |
610 | 220
611 | 537
612 |
613 |
614 | 216
615 | 505
616 |
617 |
618 |
619 |
620 | button_set_treeish
621 | clicked()
622 | view_head
623 | rebuild_treeish()
624 |
625 |
626 | 455
627 | 532
628 |
629 |
630 | 450
631 | 495
632 |
633 |
634 |
635 |
636 | action_about
637 | triggered()
638 | MainWindow
639 | show_about_dialog()
640 |
641 |
642 | -1
643 | -1
644 |
645 |
646 | 399
647 | 299
648 |
649 |
650 |
651 |
652 |
653 | open_repo()
654 | refresh_repo()
655 | clear_header_menu()
656 | create_header_menu_action(QString)
657 | show_about_dialog()
658 |
659 |
660 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Git-Annex-Metadata-Gui
4 | # Copyright (C) 2017 Alper Nebi Yasak
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import os
20 | import codecs
21 | import setuptools
22 |
23 | root = os.path.abspath(os.path.dirname(__file__))
24 | with open(os.path.join(root, 'README.rst'), encoding='utf-8') as f:
25 | readme = f.read()
26 |
27 | setuptools.setup(
28 | name='git-annex-metadata-gui',
29 | version='0.2.0',
30 | description='Graphical interface for git-annex metadata commands',
31 | long_description=readme,
32 | url='https://github.com/alpernebbi/git-annex-metadata-gui',
33 | author='Alper Nebi Yasak',
34 | author_email='alpernebiyasak@gmail.com',
35 | license='GPL3+',
36 | classifiers=[
37 | 'Development Status :: 3 - Alpha',
38 | 'Environment :: X11 Applications :: Qt',
39 | 'Intended Audience :: End Users/Desktop',
40 | 'Topic :: Utilities',
41 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
42 | 'Programming Language :: Python :: 3',
43 | 'Programming Language :: Python :: 3.5',
44 | ],
45 | entry_points={
46 | 'gui_scripts': [
47 | 'git-annex-metadata-gui=git_annex_metadata_gui:main',
48 | ],
49 | },
50 | keywords='git-annex metadata',
51 | packages=['git_annex_metadata_gui'],
52 | install_requires=['PyQt5', 'git-annex-adapter>=0.2.0'],
53 | )
54 |
--------------------------------------------------------------------------------