From 08cee57db8fe487f4dc08417d7084741b469fd64 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 14:09:57 +0200 Subject: [PATCH 01/69] empty __init__ cleanup --- CodeReview/Application/__init__.py | 17 ----------------- CodeReview/Config/__init__.py | 17 ----------------- CodeReview/Diff/__init__.py | 17 ----------------- CodeReview/GUI/LogBrowser/__init__.py | 17 ----------------- CodeReview/GUI/__init__.py | 17 ----------------- CodeReview/__init__.py | 17 ----------------- 6 files changed, 102 deletions(-) diff --git a/CodeReview/Application/__init__.py b/CodeReview/Application/__init__.py index f523b65..e69de29 100644 --- a/CodeReview/Application/__init__.py +++ b/CodeReview/Application/__init__.py @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/Config/__init__.py b/CodeReview/Config/__init__.py index f523b65..e69de29 100644 --- a/CodeReview/Config/__init__.py +++ b/CodeReview/Config/__init__.py @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/Diff/__init__.py b/CodeReview/Diff/__init__.py index f523b65..e69de29 100644 --- a/CodeReview/Diff/__init__.py +++ b/CodeReview/Diff/__init__.py @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/GUI/LogBrowser/__init__.py b/CodeReview/GUI/LogBrowser/__init__.py index f523b65..e69de29 100644 --- a/CodeReview/GUI/LogBrowser/__init__.py +++ b/CodeReview/GUI/LogBrowser/__init__.py @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/GUI/__init__.py b/CodeReview/GUI/__init__.py index f523b65..e69de29 100644 --- a/CodeReview/GUI/__init__.py +++ b/CodeReview/GUI/__init__.py @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/__init__.py b/CodeReview/__init__.py index f523b65..e69de29 100644 --- a/CodeReview/__init__.py +++ b/CodeReview/__init__.py @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### From 2e279b24e17f3b240b9f58c8edb53bc66d6d4df2 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 14:10:29 +0200 Subject: [PATCH 02/69] cleanup --- CodeReview/Application/ApplicationBase.py | 3 -- CodeReview/Config/ConfigInstall.py | 1 - .../GUI/DiffViewer/DiffViewerApplication.py | 4 -- .../GUI/DiffViewer/DiffViewerMainWindow.py | 35 +++++++-------- CodeReview/GUI/LogBrowser/CommitTableModel.py | 5 --- .../GUI/LogBrowser/LogBrowserApplication.py | 6 --- .../GUI/LogBrowser/LogBrowserMainWindow.py | 22 +++++----- CodeReview/GUI/LogBrowser/LogTableModel.py | 4 -- CodeReview/Repository/Git.py | 10 ++--- bin/diff-viewer | 44 ++++++++++++------- bin/pyqgit | 33 ++++++++------ 11 files changed, 74 insertions(+), 93 deletions(-) diff --git a/CodeReview/Application/ApplicationBase.py b/CodeReview/Application/ApplicationBase.py index 9af7839..58cc0c0 100644 --- a/CodeReview/Application/ApplicationBase.py +++ b/CodeReview/Application/ApplicationBase.py @@ -69,7 +69,6 @@ def _exception_hook(self, exception_type, exception_value, exception_traceback): ############################################## def execute_given_user_script(self): - if self._args.user_script is not None: self.execute_user_script(self._args.user_script) @@ -91,11 +90,9 @@ def execute_user_script(self, file_name): ############################################## def exit(self): - sys.exit(0) ############################################## def show_message(self, message=None, **kwargs): - self._logger.info(message) diff --git a/CodeReview/Config/ConfigInstall.py b/CodeReview/Config/ConfigInstall.py index df21be2..260b5f6 100644 --- a/CodeReview/Config/ConfigInstall.py +++ b/CodeReview/Config/ConfigInstall.py @@ -39,7 +39,6 @@ class Logging: @staticmethod def find(config_file): - return PathTools.find(config_file, Logging.directories) #################################################################################################### diff --git a/CodeReview/GUI/DiffViewer/DiffViewerApplication.py b/CodeReview/GUI/DiffViewer/DiffViewerApplication.py index 7ae041e..9b62dd3 100644 --- a/CodeReview/GUI/DiffViewer/DiffViewerApplication.py +++ b/CodeReview/GUI/DiffViewer/DiffViewerApplication.py @@ -52,13 +52,11 @@ def __init__(self, args): ############################################## def _init_actions(self): - super(DiffViewerApplication, self)._init_actions() ############################################## def post_init(self): - super(DiffViewerApplication, self).post_init() self._main_window.open_files(self._args.file1, self._args.file2, self._args.show) self._init_file_system_watcher() @@ -81,9 +79,7 @@ def show_message(self, message=None, timeout=0, warn=False): ############################################## def _init_file_system_watcher(self): - # Fixme: catch all events - self._file_system_watcher = QtCore.QFileSystemWatcher() self._file_system_watcher.directoryChanged.connect(self.directory_changed) self._file_system_watcher.fileChanged.connect(self.file_changed) diff --git a/CodeReview/GUI/DiffViewer/DiffViewerMainWindow.py b/CodeReview/GUI/DiffViewer/DiffViewerMainWindow.py index 96f8990..77cfbb1 100644 --- a/CodeReview/GUI/DiffViewer/DiffViewerMainWindow.py +++ b/CodeReview/GUI/DiffViewer/DiffViewerMainWindow.py @@ -168,13 +168,13 @@ def _create_actions(self): # Fixme: only git if self._repository: self._stage_action = \ - QtWidgets.QAction('Stage', - self, - toolTip='Stage file', - shortcut='Ctrl+S', - checkable=True, - triggered=self._stage, - ) + QtWidgets.QAction('Stage', + self, + toolTip='Stage file', + shortcut='Ctrl+S', + checkable=True, + triggered=self._stage, + ) else: self._stage_action = None @@ -245,7 +245,6 @@ def _create_toolbar(self): ############################################## def init_menu(self): - super().init_menu() ############################################## @@ -303,20 +302,20 @@ def open_files(self, file1, file2, show=False): paths = (file1, file2) texts = (None, None) - metadatas = [dict(path=file1, document_type='file', last_modification_date=None), - dict(path=file2, document_type='file', last_modification_date=None)] + metadatas = [ + dict(path=file1, document_type='file', last_modification_date=None), + dict(path=file2, document_type='file', last_modification_date=None), + ] self.diff_documents(paths, texts, metadatas, show=show) ############################################## def _absolute_path(self, path): - return os.path.join(self._workdir, path) ############################################## def _read_file(self, path): - with open(self._absolute_path(path)) as f: text = f.read() return text @@ -324,7 +323,6 @@ def _read_file(self, path): ############################################## def _is_directory(self, path): - if path is None: return False else: @@ -333,10 +331,8 @@ def _is_directory(self, path): ############################################## def _get_lexer(self, path, text): - if path is None: return None - return self._lexer_cache.guess(path, text) ############################################## @@ -394,8 +390,10 @@ def diff_text_documents(self, show=False): self._highlighted_documents = [] if not show: - file_diff = TwoWayFileDiffFactory().process(* raw_text_documents, - number_of_lines_of_context=number_of_lines_of_context) + file_diff = TwoWayFileDiffFactory().process( + * raw_text_documents, + number_of_lines_of_context=number_of_lines_of_context, + ) document_models = TextDocumentDiffModelFactory().process(file_diff) for raw_text_document, document_model, lexer in zip(raw_text_documents, document_models, lexers): if lexer is not None and highlight: @@ -436,7 +434,6 @@ def _set_document_models(self): ############################################## def _on_font_size_change(self, index=None, refresh=True): - self._diff_view.set_font(self._font_size_combobox.currentData()) if refresh: self._refresh() # Fixme: block position are not updated @@ -444,9 +441,7 @@ def _on_font_size_change(self, index=None, refresh=True): ############################################## def _on_file_system_changed(self, path): - # only used for main window - self._logger.info(path) self._refresh() diff --git a/CodeReview/GUI/LogBrowser/CommitTableModel.py b/CodeReview/GUI/LogBrowser/CommitTableModel.py index 84a4ee8..2e4ed7d 100644 --- a/CodeReview/GUI/LogBrowser/CommitTableModel.py +++ b/CodeReview/GUI/LogBrowser/CommitTableModel.py @@ -95,14 +95,12 @@ def update(self, diff): ############################################## def __iter__(self): - for row in self._rows: yield row[-1] ############################################## def __getitem__(self, i): - return self._rows[i][-1] ############################################## @@ -150,19 +148,16 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): ############################################## def columnCount(self, index=QtCore.QModelIndex()): - return len(self.__titles__) ############################################## def rowCount(self, index=QtCore.QModelIndex()): - return self._number_of_rows ############################################## def sort(self, column, order): - reverse = order == Qt.DescendingOrder self._rows.sort(key=lambda x:x[column], reverse=reverse) self.modelReset.emit() diff --git a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py index a5e8f1f..50f2f08 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py @@ -56,7 +56,6 @@ def __init__(self, args): ############################################## def _init_actions(self): - super(LogBrowserApplication, self)._init_actions() ############################################## @@ -185,7 +184,6 @@ def watch_directories(self): ############################################## def watch(self, path): - absolut_path = self._repository.join_repository_path(path) self._logger.info(absolut_path) self._file_system_watcher.addPath(absolut_path) @@ -193,7 +191,6 @@ def watch(self, path): ############################################## def unwatch(self, path): - absolut_path = self._repository.join_repository_path(path) self._logger.info(absolut_path) self._file_system_watcher.removePath(absolut_path) @@ -201,7 +198,6 @@ def unwatch(self, path): ############################################## def _unwatch_paths(self, paths): - # Fixme: code ??? if paths: self._file_system_watcher.removePaths(paths) @@ -209,11 +205,9 @@ def _unwatch_paths(self, paths): ############################################## def unwatch_directories(self): - self._unwatch_paths(self._file_system_watcher.directories()) ############################################## def unwatch_files(self): - self._unwatch_paths(self._file_system_watcher.files()) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index aafeb39..41ee5c3 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -172,7 +172,6 @@ def _create_toolbar(self): ############################################## def init_menu(self): - super(LogBrowserMainWindow, self).init_menu() ############################################## @@ -258,7 +257,6 @@ def show_working_tree_diff(self): ############################################## def _update_working_tree_diff(self): - # Check log table is on working tree if self._log_table.currentIndex().row() == 0: self._update_commit_table() @@ -303,7 +301,6 @@ def _update_commit_table(self, index=None): ############################################## def _on_clicked_table(self, index): - # called when a commit row is clicked self._logger.info('') self._current_patch_index = index.row() @@ -337,7 +334,6 @@ def _create_diff_viewer_window(self): ############################################## def _on_diff_window_closed(self): - self._application.unwatch_files() # Fixme: only current patch ! self._diff_window = None self._logger.info("Diff Viewer closed") @@ -346,7 +342,7 @@ def _on_diff_window_closed(self): def _show_patch(self, patch): - self._logger.info("") + self._logger.info('') self._application.unwatch_files() @@ -368,23 +364,26 @@ def _show_patch(self, patch): elif delta.status == git.GIT_DELTA_DELETED: paths = (old_path, None) repository = self._application.repository - texts = [repository.file_content(blob_id) - for blob_id in (delta.old_file.id, delta.new_file.id)] - metadatas = [dict(path=old_path, document_type='file', last_modification_date=None), - dict(path=new_path, document_type='file', last_modification_date=None)] + texts = [ + repository.file_content(blob_id) + for blob_id in (delta.old_file.id, delta.new_file.id) + ] + metadatas = [ + dict(path=old_path, document_type='file', last_modification_date=None), + dict(path=new_path, document_type='file', last_modification_date=None), + ] self._diff_window.diff_documents(paths, texts, metadatas, workdir=repository.workdir) self._application.watch(new_path) else: self._logger.info('revision {} Binary '.format(self._current_revision) + new_path) - # Fixme: show image pdf ... + # Fixme: show image pdf ... # Monitor file change ############################################## @property def _last_path_index(self): - return len(self._diff) -1 ############################################## @@ -419,7 +418,6 @@ def next_patch(self): ############################################## def reload_current_patch(self): - if self._current_patch_index is not None: patch = self._diff[self._current_patch_index] self._show_patch(patch) diff --git a/CodeReview/GUI/LogBrowser/LogTableModel.py b/CodeReview/GUI/LogBrowser/LogTableModel.py index 20cbec3..0e7fa64 100644 --- a/CodeReview/GUI/LogBrowser/LogTableModel.py +++ b/CodeReview/GUI/LogBrowser/LogTableModel.py @@ -65,7 +65,6 @@ def __init__(self, repository): ############################################## def _commit_data(self, i, commit): - return ( self._number_of_rows - i -1, commit.message, @@ -78,7 +77,6 @@ def _commit_data(self, i, commit): ############################################## def __getitem__(self, i): - return self._rows[i][-1] ############################################## @@ -116,11 +114,9 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): ############################################## def columnCount(self, index=QtCore.QModelIndex()): - return len(self.__titles__) ############################################## def rowCount(self, index=QtCore.QModelIndex()): - return self._number_of_rows diff --git a/CodeReview/Repository/Git.py b/CodeReview/Repository/Git.py index 91e1f4e..809e197 100644 --- a/CodeReview/Repository/Git.py +++ b/CodeReview/Repository/Git.py @@ -127,7 +127,7 @@ def file_content(self, oid): ############################################## - __STATUS_TEXT__ = { + _STATUS_TEXT = { git.GIT_STATUS_CONFLICTED: 'conflicted', git.GIT_STATUS_CURRENT: 'current', git.GIT_STATUS_IGNORED: 'ignored', @@ -143,8 +143,8 @@ def status(self, path): try: status = self.repository_status[path] - status_text = ' | '.join([self.__STATUS_TEXT__[bit] - for bit in self.__STATUS_TEXT__ + status_text = ' | '.join([self._STATUS_TEXT[bit] + for bit in self._STATUS_TEXT if status & bit]) self._logger.info("File {} has status {} / {}".format(path, status, status_text)) return status @@ -168,13 +168,11 @@ def is_staged(self, path): ############################################## def is_modified(self, path): - return self.status(path) == git.GIT_STATUS_WT_MODIFIED ############################################## def stage(self, path): - index = self.index index.add(path) index.write() @@ -202,7 +200,6 @@ class Diff: ############################################## def __init__(self, diff, patches): - self.diff = diff self._patches = patches @@ -224,5 +221,4 @@ def __getitem__(self, i): ############################################## def new_paths(self): - return [patch.delta.new_file.path for patch in self._patches] diff --git a/bin/diff-viewer b/bin/diff-viewer index a8254e5..c44d25d 100755 --- a/bin/diff-viewer +++ b/bin/diff-viewer @@ -1,5 +1,4 @@ #! /usr/bin/env python3 -# -*- python -*- #################################################################################################### # @@ -46,22 +45,33 @@ from CodeReview.Tools.ProgramOptions import PathAction argument_parser = argparse.ArgumentParser(description='Diff Viewer') -argument_parser.add_argument('file1', metavar='File1', - help='First file') - -argument_parser.add_argument('file2', metavar='File2', - help='Second File') - -argument_parser.add_argument('--show', action='store_true') - -argument_parser.add_argument('--user-script', - action=PathAction, - default=None, - help='user script to execute') - -argument_parser.add_argument('--user-script-args', - default='', - help="user script args (don't forget to quote)") +argument_parser.add_argument( + 'file1', metavar='File1', + help='First file', +) + +argument_parser.add_argument( + 'file2', metavar='File2', + help='Second File', +) + +argument_parser.add_argument( + '--show', + action='store_true', +) + +argument_parser.add_argument( + '--user-script', + action=PathAction, + default=None, + help='user script to execute', +) + +argument_parser.add_argument( + '--user-script-args', + default='', + help="user script args (don't forget to quote)", +) args = argument_parser.parse_args() diff --git a/bin/pyqgit b/bin/pyqgit index cc416e4..6f5ead1 100755 --- a/bin/pyqgit +++ b/bin/pyqgit @@ -1,5 +1,4 @@ #! /usr/bin/env python3 -# -*- python -*- #################################################################################################### # @@ -46,19 +45,25 @@ from CodeReview.Tools.ProgramOptions import PathAction argument_parser = argparse.ArgumentParser(description='Log Browser') -argument_parser.add_argument('path', metavar='PATH', - action=PathAction, - nargs='?', default='.', - help='path') - -argument_parser.add_argument('--user-script', - action=PathAction, - default=None, - help='user script to execute') - -argument_parser.add_argument('--user-script-args', - default='', - help="user script args (don't forget to quote)") +argument_parser.add_argument( + 'path', metavar='PATH', + action=PathAction, + nargs='?', default='.', + help='path', +) + +argument_parser.add_argument( + '--user-script', + action=PathAction, + default=None, + help='user script to execute', +) + +argument_parser.add_argument( + '--user-script-args', + default='', + help="user script args (don't forget to quote)", +) args = argument_parser.parse_args() From 388272f9c3521b215125a86879f2af4f444db0d1 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 14:11:05 +0200 Subject: [PATCH 03/69] setenv --- setenv.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setenv.sh b/setenv.sh index 043bdad..d2e5ec5 100644 --- a/setenv.sh +++ b/setenv.sh @@ -1,6 +1,6 @@ source /opt/python-virtual-env/py36/bin/activate # append_to_ld_library_path_if_not /usr/local/lib -append_to_ld_library_path_if_not /usr/local/stow/libgit2-26/lib +append_to_ld_library_path_if_not /usr/local/stow/libgit2-27/lib append_to_python_path_if_not ${PWD} From cab593bb15b200c8d9096e74c8c17f46719d6318 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 14:13:55 +0200 Subject: [PATCH 04/69] cleanup --- CodeReview/GUI/LogBrowser/CommitTableModel.py | 10 +++++----- CodeReview/GUI/LogBrowser/LogTableModel.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/CommitTableModel.py b/CodeReview/GUI/LogBrowser/CommitTableModel.py index 2e4ed7d..40028a3 100644 --- a/CodeReview/GUI/LogBrowser/CommitTableModel.py +++ b/CodeReview/GUI/LogBrowser/CommitTableModel.py @@ -46,14 +46,14 @@ class CommitTableModel(QtCore.QAbstractTableModel): 'similarity', )) - __titles__ = ( + _TITLES = ( 'Modification', 'Old Path', 'New Path', 'Similarity', ) - __status_to_letter__ = { + _STATUS_TO_LETTER = { git.GIT_DELTA_DELETED: 'D', git.GIT_DELTA_MODIFIED: 'M', git.GIT_DELTA_ADDED: 'A', @@ -84,7 +84,7 @@ def update(self, diff): else: new_file_path = '' similarity = '' - status = self.__status_to_letter__[delta.status] + status = self._STATUS_TO_LETTER[delta.status] row = (status, delta.old_file.path, new_file_path, similarity, patch) self._rows.append(row) @@ -141,14 +141,14 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: - return QtCore.QVariant(self.__titles__[section]) + return QtCore.QVariant(self._TITLES[section]) return QtCore.QVariant() ############################################## def columnCount(self, index=QtCore.QModelIndex()): - return len(self.__titles__) + return len(self._TITLES) ############################################## diff --git a/CodeReview/GUI/LogBrowser/LogTableModel.py b/CodeReview/GUI/LogBrowser/LogTableModel.py index 0e7fa64..53b6a95 100644 --- a/CodeReview/GUI/LogBrowser/LogTableModel.py +++ b/CodeReview/GUI/LogBrowser/LogTableModel.py @@ -42,7 +42,7 @@ class LogTableModel(QtCore.QAbstractTableModel): 'comitter', )) - __titles__ = ( + _TITLES = ( 'Revision', 'Message', 'Id SH1', @@ -105,7 +105,7 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: - return QtCore.QVariant(self.__titles__[section]) + return QtCore.QVariant(self._TITLES[section]) else: return QtCore.QVariant(self._number_of_rows - section) @@ -114,7 +114,7 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): ############################################## def columnCount(self, index=QtCore.QModelIndex()): - return len(self.__titles__) + return len(self._TITLES) ############################################## From 1c3fa0869fc67cc0df83133b253830505b79746e Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 18:30:11 +0200 Subject: [PATCH 05/69] implement committer filter and add tag --- .../GUI/LogBrowser/LogBrowserApplication.py | 24 +++++-- .../GUI/LogBrowser/LogBrowserMainWindow.py | 35 ++++++++-- CodeReview/GUI/LogBrowser/LogTableModel.py | 64 +++++++++++++++++-- CodeReview/Repository/Git.py | 12 ++++ 4 files changed, 117 insertions(+), 18 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py index 50f2f08..2bf9d01 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py @@ -16,6 +16,9 @@ # #################################################################################################### +# Fixme: move to a QML application +# QmlRepository < QmlLog < QmlCommit < QmlPatch + ################################################################################################### import logging @@ -29,7 +32,7 @@ from CodeReview.GUI.Base.GuiApplicationBase import GuiApplicationBase from CodeReview.Repository.Git import RepositoryNotFound, GitRepository from .CommitTableModel import CommitTableModel -from .LogTableModel import LogTableModel +from .LogTableModel import LogTableModel, LogTableFilterProxyModel #################################################################################################### @@ -86,6 +89,8 @@ def show_message(self, message=None, timeout=0, warn=False): def _init_repository(self): + # Fixme: code place + self._logger.info('Init Repository') if self._args.path is None: @@ -100,10 +105,13 @@ def _init_repository(self): return self._log_table_model = LogTableModel(self._repository) + self._log_table_filter = LogTableFilterProxyModel() + self._log_table_filter.setSourceModel(self._log_table_model) log_table = self._main_window._log_table - log_table.setModel(self._log_table_model) + # log_table.setModel(self._log_table_model) + log_table.setModel(self._log_table_filter) # Set the column widths - column_enum = self._log_table_model.column_enum + column_enum = self._log_table_model.COLUMN_ENUM width = 0 for column in ( column_enum.revision, @@ -134,6 +142,14 @@ def reload_repository(self): def repository(self): return self._repository + @property + def log_table_model(self): + return self._log_table_model + + @property + def log_table_filter(self): + return self._log_table_filter + ############################################## def _init_file_system_watcher(self): @@ -178,7 +194,7 @@ def watch_directories(self): for root, _, _ in os.walk(self._repository.workdir): if not root.startswith(git_path): paths.append(root) - self._logger.info('watch {}'.format(paths)) + # self._logger.info('watch {}'.format(paths)) self._file_system_watcher.addPaths(paths) ############################################## diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index 41ee5c3..334fe09 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -22,7 +22,7 @@ import os from PyQt5 import QtWidgets -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QRegExp import pygit2 as git @@ -31,6 +31,7 @@ from CodeReview.GUI.Base.MainWindowBase import MainWindowBase from CodeReview.GUI.Widgets.IconLoader import IconLoader from CodeReview.GUI.Widgets.MessageBox import MessageBox +from .LogTableModel import LogTableModel #################################################################################################### @@ -73,16 +74,26 @@ def _init_ui(self): self.setCentralWidget(self._central_widget) self._vertical_layout = QtWidgets.QVBoxLayout(self._central_widget) + self._message_box = MessageBox(self) + + horizontal_layout = QtWidgets.QHBoxLayout() + label = QtWidgets.QLabel('Filter') + self._commit_filter = QtWidgets.QLineEdit() + self._commit_filter.textChanged.connect(self._on_filter_changed) + for widget in (label, self._commit_filter): + horizontal_layout.addWidget(widget) + splitter = QtWidgets.QSplitter() splitter.setOrientation(Qt.Vertical) self._log_table = QtWidgets.QTableView() self._commit_table = QtWidgets.QTableView() + for widget in (self._log_table, self._commit_table): + splitter.addWidget(widget) + self._vertical_layout.addLayout(horizontal_layout) for widget in (self._message_box, splitter): self._vertical_layout.addWidget(widget) - for widget in (self._log_table, self._commit_table): - splitter.addWidget(widget) table = self._log_table table.setSelectionMode(QtWidgets.QTableView.SingleSelection) @@ -106,7 +117,6 @@ def _init_ui(self): ############################################## def finish_table_connections(self): - self._log_table.selectionModel().currentRowChanged.connect(self._update_commit_table) #!# Fixme: reopen diff viewer window when repository change #!# self._commit_table.selectionModel().currentRowChanged.connect(self._on_clicked_table) @@ -162,7 +172,6 @@ def _create_actions(self): ############################################## def _create_toolbar(self): - self._tool_bar = self.addToolBar('Diff on Working Tree') for item in self._action_group.actions(): self._tool_bar.addAction(item) @@ -266,13 +275,15 @@ def _update_working_tree_diff(self): def _update_commit_table(self, index=None): if index is not None: + index = self._application.log_table_filter.mapToSource(index) index = index.row() else: index = 0 if index: self._current_revision = index - log_table_model = self._log_table.model() + # log_table_model = self._log_table.model() + log_table_model = self._application.log_table_model commit1 = log_table_model[index] try: commit2 = log_table_model[index +1] @@ -292,7 +303,10 @@ def _update_commit_table(self, index=None): kwargs = dict(a='HEAD') self._diff_kwargs = kwargs - self._diff = self._application.repository.diff(**kwargs) + self._logger.info('index {} kwargs {}'.format(index, kwargs)) + # Fixme: + if kwargs['a'] is not None: + self._diff = self._application.repository.diff(**kwargs) commit_table_model = self._commit_table.model() commit_table_model.update(self._diff) @@ -421,3 +435,10 @@ def reload_current_patch(self): if self._current_patch_index is not None: patch = self._diff[self._current_patch_index] self._show_patch(patch) + + ############################################## + + def _on_filter_changed(self, text): + log_table_filter = self._application.log_table_filter + log_table_filter.setFilterRegExp(QRegExp(text, Qt.CaseInsensitive, QRegExp.FixedString)) + log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.comitter) diff --git a/CodeReview/GUI/LogBrowser/LogTableModel.py b/CodeReview/GUI/LogBrowser/LogTableModel.py index 53b6a95..53a61b6 100644 --- a/CodeReview/GUI/LogBrowser/LogTableModel.py +++ b/CodeReview/GUI/LogBrowser/LogTableModel.py @@ -16,6 +16,8 @@ # #################################################################################################### +__all__ = ['LogTableFilterProxyModel', 'LogTableModel'] + #################################################################################################### import datetime @@ -32,9 +34,32 @@ #################################################################################################### +class LogTableFilterProxyModel(QtCore.QSortFilterProxyModel): + + ############################################## + + def __init__(self, parent=None): + super().__init__(parent) + + ############################################## + + def __getitem__(self, row): + # Fixme: don't work ??? + model = self.sourceModel() + index = model.createIndex(row, 0) + index = self.mapToSource(index) + row = index.row() + return model[row] + + ############################################## + + # def filterAcceptsRow(source_row, source_parent): + +#################################################################################################### + class LogTableModel(QtCore.QAbstractTableModel): - column_enum = EnumFactory('LogColumnEnum', ( + COLUMN_ENUM = EnumFactory('LogColumnEnum', ( 'revision', 'message', 'sha', @@ -54,23 +79,48 @@ class LogTableModel(QtCore.QAbstractTableModel): def __init__(self, repository): - super(LogTableModel, self).__init__() + super().__init__() - commits = repository.commits() + self._tags = repository.tags + commits = repository.commits self._number_of_rows = len(commits) self._rows = [('', 'Working directory changes', '', '', None)] - self._rows.extend([self._commit_data(i, commit) - for i, commit in enumerate(commits)]) + for i, commit in enumerate(commits): + row = self._commit_data(i, commit) + self._rows.append(row) + + ############################################## + + def _match_tag(self, commit): + + for ref in self._tags: + ref_commit = ref.peel() + if commit.id == ref_commit.id: + return ref + return None ############################################## def _commit_data(self, i, commit): + + ref = self._match_tag(commit) + if ref is not None: + tag_name = ref.name + tag_name = tag_name.replace('refs/tags/', '') + tag_name = '[{}] '.format(tag_name) + else: + tag_name = '' + + author = commit.author + committer = commit.committer + return ( self._number_of_rows - i -1, - commit.message, + tag_name + commit.message, commit.hex, fromtimestamp(commit.commit_time).strftime('%Y-%m-%d %H:%M:%S'), - commit.committer.name, # author + '{} <{}>'.format(committer.name, committer.email), + commit, ) diff --git a/CodeReview/Repository/Git.py b/CodeReview/Repository/Git.py index 809e197..f409416 100644 --- a/CodeReview/Repository/Git.py +++ b/CodeReview/Repository/Git.py @@ -20,6 +20,7 @@ import logging import os +import re import pygit2 as git @@ -84,6 +85,17 @@ def join_repository_path(self, path): ############################################## + @property + def tags(self): + regex = re.compile('^refs/tags') + return [ + self._repository.references[name] + for name in self._repository.references if regex.match(name) + ] + + ############################################## + + @property def commits(self): head = self._repository.head From 146fb287ea08dcdca9d1f2cddf7d0aafefa8016e Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 18:31:34 +0200 Subject: [PATCH 06/69] typo --- CodeReview/GUI/LogBrowser/LogBrowserApplication.py | 2 +- CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py | 2 +- CodeReview/GUI/LogBrowser/LogTableModel.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py index 2bf9d01..3153867 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py @@ -117,7 +117,7 @@ def _init_repository(self): column_enum.revision, # column_enum.message, column_enum.date, - column_enum.comitter, + column_enum.committer, ): log_table.resizeColumnToContents(int(column)) width += log_table.columnWidth(int(column)) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index 334fe09..613a22b 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -441,4 +441,4 @@ def reload_current_patch(self): def _on_filter_changed(self, text): log_table_filter = self._application.log_table_filter log_table_filter.setFilterRegExp(QRegExp(text, Qt.CaseInsensitive, QRegExp.FixedString)) - log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.comitter) + log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.committer) diff --git a/CodeReview/GUI/LogBrowser/LogTableModel.py b/CodeReview/GUI/LogBrowser/LogTableModel.py index 53a61b6..ac8dfc6 100644 --- a/CodeReview/GUI/LogBrowser/LogTableModel.py +++ b/CodeReview/GUI/LogBrowser/LogTableModel.py @@ -64,7 +64,7 @@ class LogTableModel(QtCore.QAbstractTableModel): 'message', 'sha', 'date', - 'comitter', + 'committer', )) _TITLES = ( @@ -72,7 +72,7 @@ class LogTableModel(QtCore.QAbstractTableModel): 'Message', 'Id SH1', 'Date', - 'Comitter', + 'Committer', ) ############################################## From 38fdc8ffa237c45d888fdc95e6bb8573475576f4 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 18:54:20 +0200 Subject: [PATCH 07/69] add message and sha filter --- .../GUI/LogBrowser/LogBrowserMainWindow.py | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index 613a22b..7d2a1dd 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -76,24 +76,47 @@ def _init_ui(self): self._vertical_layout = QtWidgets.QVBoxLayout(self._central_widget) self._message_box = MessageBox(self) + self._vertical_layout.addWidget(self._message_box) + + row = 0 + layout = QtWidgets.QGridLayout() + self._vertical_layout.addLayout(layout) + label = QtWidgets.QLabel('Committer Filter') + committer_filter = QtWidgets.QLineEdit() + committer_filter.textChanged.connect(self._on_committer_filter_changed) + for i, widget in enumerate((label, committer_filter)): + layout.addWidget(widget, row, i) + row += 1 horizontal_layout = QtWidgets.QHBoxLayout() - label = QtWidgets.QLabel('Filter') - self._commit_filter = QtWidgets.QLineEdit() - self._commit_filter.textChanged.connect(self._on_filter_changed) - for widget in (label, self._commit_filter): - horizontal_layout.addWidget(widget) + self._vertical_layout.addLayout(horizontal_layout) + label = QtWidgets.QLabel('Message Filter') + message_filter = QtWidgets.QLineEdit() + message_filter.textChanged.connect(self._on_message_filter_changed) + for i, widget in enumerate((label, message_filter)): + layout.addWidget(widget, row, i) + row += 1 + + horizontal_layout = QtWidgets.QHBoxLayout() + self._vertical_layout.addLayout(horizontal_layout) + label = QtWidgets.QLabel('SHA Filter') + sha_filter = QtWidgets.QLineEdit() + sha_filter.textChanged.connect(self._on_sha_filter_changed) + for i, widget in enumerate((label, sha_filter)): + layout.addWidget(widget, row, i) + row += 1 splitter = QtWidgets.QSplitter() + self._vertical_layout.addWidget(splitter) splitter.setOrientation(Qt.Vertical) self._log_table = QtWidgets.QTableView() self._commit_table = QtWidgets.QTableView() for widget in (self._log_table, self._commit_table): splitter.addWidget(widget) - self._vertical_layout.addLayout(horizontal_layout) - for widget in (self._message_box, splitter): - self._vertical_layout.addWidget(widget) + # self._vertical_layout.addLayout(horizontal_layout) + # for widget in (self._message_box, splitter): + # self._vertical_layout.addWidget(widget) table = self._log_table table.setSelectionMode(QtWidgets.QTableView.SingleSelection) @@ -438,7 +461,27 @@ def reload_current_patch(self): ############################################## - def _on_filter_changed(self, text): + def _on_committer_filter_changed(self, text): log_table_filter = self._application.log_table_filter log_table_filter.setFilterRegExp(QRegExp(text, Qt.CaseInsensitive, QRegExp.FixedString)) log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.committer) + + ############################################## + + def _on_message_filter_changed(self, text): + log_table_filter = self._application.log_table_filter + log_table_filter.setFilterRegExp(QRegExp(text, Qt.CaseInsensitive, QRegExp.FixedString)) + log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.message) + + ############################################## + + def _on_sha_filter_changed(self, text): + log_table_filter = self._application.log_table_filter + if text: + # Fixme: ??? + # regexp = '^' + text + regexp = text + else: + regexp = '' + log_table_filter.setFilterRegExp(QRegExp(regexp, Qt.CaseInsensitive, QRegExp.FixedString)) + log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.sha) From 70505db5ad33284144ed39396d15cb9d2111b0cd Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 18:54:50 +0200 Subject: [PATCH 08/69] setenv --- setenv.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setenv.sh b/setenv.sh index d2e5ec5..4f17c0f 100644 --- a/setenv.sh +++ b/setenv.sh @@ -1,4 +1,4 @@ -source /opt/python-virtual-env/py36/bin/activate +source /opt/python-virtual-env/py38/bin/activate # append_to_ld_library_path_if_not /usr/local/lib append_to_ld_library_path_if_not /usr/local/stow/libgit2-27/lib From 2492805633865086e16d527bb29f967388f03194 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 20:23:25 +0200 Subject: [PATCH 09/69] add commit count --- CodeReview/GUI/LogBrowser/LogBrowserApplication.py | 2 ++ CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py index 3153867..a4b34ec 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py @@ -107,6 +107,8 @@ def _init_repository(self): self._log_table_model = LogTableModel(self._repository) self._log_table_filter = LogTableFilterProxyModel() self._log_table_filter.setSourceModel(self._log_table_model) + # self._log_table_filter.???.connect(self._main_window._on_log_table_filter_changed) + self._main_window._on_log_table_filter_changed() log_table = self._main_window._log_table # log_table.setModel(self._log_table_model) log_table.setModel(self._log_table_filter) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index 7d2a1dd..240bda4 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -106,6 +106,9 @@ def _init_ui(self): layout.addWidget(widget, row, i) row += 1 + self._row_count = QtWidgets.QLabel('') + self._vertical_layout.addWidget(self._row_count) + splitter = QtWidgets.QSplitter() self._vertical_layout.addWidget(splitter) splitter.setOrientation(Qt.Vertical) @@ -465,6 +468,7 @@ def _on_committer_filter_changed(self, text): log_table_filter = self._application.log_table_filter log_table_filter.setFilterRegExp(QRegExp(text, Qt.CaseInsensitive, QRegExp.FixedString)) log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.committer) + self._on_log_table_filter_changed() # seems to just work, no need to connect signal ############################################## @@ -472,6 +476,7 @@ def _on_message_filter_changed(self, text): log_table_filter = self._application.log_table_filter log_table_filter.setFilterRegExp(QRegExp(text, Qt.CaseInsensitive, QRegExp.FixedString)) log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.message) + self._on_log_table_filter_changed() ############################################## @@ -485,3 +490,12 @@ def _on_sha_filter_changed(self, text): regexp = '' log_table_filter.setFilterRegExp(QRegExp(regexp, Qt.CaseInsensitive, QRegExp.FixedString)) log_table_filter.setFilterKeyColumn(LogTableModel.COLUMN_ENUM.sha) + self._on_log_table_filter_changed() + + ############################################## + + def _on_log_table_filter_changed(self): + log_table_filter = self._application.log_table_filter + self._row_count.setText('{} commits'.format(log_table_filter.rowCount())) + + From f38ec3b118695cca063c12c1b366a2ca651cdd6f Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 20:49:49 +0200 Subject: [PATCH 10/69] fix _update_commit_table and add branch name --- .../GUI/LogBrowser/LogBrowserApplication.py | 4 ++++ .../GUI/LogBrowser/LogBrowserMainWindow.py | 23 ++++++++++++++++--- CodeReview/Repository/Git.py | 6 +++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py index a4b34ec..9ba113c 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py @@ -104,6 +104,10 @@ def _init_repository(self): self._repository = None return + branch_name = self._repository.branch_name + branch_name = branch_name.replace('refs/heads/', ' refs/heads/ ') + self._main_window._branch_name.setText('Branch: {}'.format(branch_name)) + self._log_table_model = LogTableModel(self._repository) self._log_table_filter = LogTableFilterProxyModel() self._log_table_filter.setSourceModel(self._log_table_model) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index 240bda4..5bf41d1 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -78,6 +78,10 @@ def _init_ui(self): self._message_box = MessageBox(self) self._vertical_layout.addWidget(self._message_box) + self._branch_name = QtWidgets.QLineEdit(self) + self._branch_name.setReadOnly(True) + self._vertical_layout.addWidget(self._branch_name) + row = 0 layout = QtWidgets.QGridLayout() self._vertical_layout.addLayout(layout) @@ -112,10 +116,19 @@ def _init_ui(self): splitter = QtWidgets.QSplitter() self._vertical_layout.addWidget(splitter) splitter.setOrientation(Qt.Vertical) + self._log_table = QtWidgets.QTableView() + splitter.addWidget(self._log_table) + + widget = QtWidgets.QWidget() + vertical_layout = QtWidgets.QVBoxLayout() + widget.setLayout(vertical_layout) + splitter.addWidget(widget) + self._commit_sha = QtWidgets.QLineEdit() + self._commit_sha.setReadOnly(True) + vertical_layout.addWidget(self._commit_sha) self._commit_table = QtWidgets.QTableView() - for widget in (self._log_table, self._commit_table): - splitter.addWidget(widget) + vertical_layout.addWidget(self._commit_table) # self._vertical_layout.addLayout(horizontal_layout) # for widget in (self._message_box, splitter): @@ -311,12 +324,16 @@ def _update_commit_table(self, index=None): # log_table_model = self._log_table.model() log_table_model = self._application.log_table_model commit1 = log_table_model[index] + sha = commit1.hex + self._commit_sha.setText('SHA {} / {}'.format(sha[:8], sha)) try: commit2 = log_table_model[index +1] kwargs = dict(a=commit2, b=commit1) # Fixme: except IndexError: kwargs = dict(a=commit1) + else: # working directory + self._commit_sha.setText('') self._current_revision = None if self._stagged_mode_action.isChecked(): # Changes between the index and your last commit @@ -331,7 +348,7 @@ def _update_commit_table(self, index=None): self._diff_kwargs = kwargs self._logger.info('index {} kwargs {}'.format(index, kwargs)) # Fixme: - if kwargs['a'] is not None: + if kwargs.get('a', None) is not None: self._diff = self._application.repository.diff(**kwargs) commit_table_model = self._commit_table.model() diff --git a/CodeReview/Repository/Git.py b/CodeReview/Repository/Git.py index f409416..062dc2b 100644 --- a/CodeReview/Repository/Git.py +++ b/CodeReview/Repository/Git.py @@ -78,6 +78,12 @@ def head(self): def repository_status(self): return self._repository.status() + @property + def branch_name(self): + # head = self._repository.lookup_reference('HEAD').resolve() + head = self._repository.head + return head.name + ############################################## def join_repository_path(self, path): From 18851d3d9c4829b81e775dbbd2f94259483c58c7 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 23:01:12 +0200 Subject: [PATCH 11/69] implement review --- .../GUI/LogBrowser/LogBrowserApplication.py | 19 +++-- .../GUI/LogBrowser/LogBrowserMainWindow.py | 74 +++++++++++++------ CodeReview/Repository/Git.py | 28 +++---- 3 files changed, 79 insertions(+), 42 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py index 9ba113c..ca8eb9c 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py @@ -21,6 +21,7 @@ ################################################################################################### +from pathlib import Path import logging import os @@ -28,11 +29,12 @@ #################################################################################################### +from .CommitTableModel import CommitTableModel +from .LogTableModel import LogTableModel, LogTableFilterProxyModel from CodeReview.Application.ApplicationBase import ApplicationBase from CodeReview.GUI.Base.GuiApplicationBase import GuiApplicationBase from CodeReview.Repository.Git import RepositoryNotFound, GitRepository -from .CommitTableModel import CommitTableModel -from .LogTableModel import LogTableModel, LogTableFilterProxyModel +from CodeReview.Review import Review #################################################################################################### @@ -104,6 +106,9 @@ def _init_repository(self): self._repository = None return + review_path = self._repository.workdir.joinpath('review.json') + self._review = Review(review_path) + branch_name = self._repository.branch_name branch_name = branch_name.replace('refs/heads/', ' refs/heads/ ') self._main_window._branch_name.setText('Branch: {}'.format(branch_name)) @@ -156,6 +161,10 @@ def log_table_model(self): def log_table_filter(self): return self._log_table_filter + @property + def review(self): + return self._review + ############################################## def _init_file_system_watcher(self): @@ -196,7 +205,7 @@ def _setup_file_system_watcher(self): def watch_directories(self): paths = [] - git_path = self._repository.join_repository_path('.git') + git_path = str(self._repository.join_repository_path('.git')) for root, _, _ in os.walk(self._repository.workdir): if not root.startswith(git_path): paths.append(root) @@ -206,14 +215,14 @@ def watch_directories(self): ############################################## def watch(self, path): - absolut_path = self._repository.join_repository_path(path) + absolut_path = str(self._repository.join_repository_path(path)) self._logger.info(absolut_path) self._file_system_watcher.addPath(absolut_path) ############################################## def unwatch(self, path): - absolut_path = self._repository.join_repository_path(path) + absolut_path = str(self._repository.join_repository_path(path)) self._logger.info(absolut_path) self._file_system_watcher.removePath(absolut_path) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index 5bf41d1..c0f0bbc 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -19,19 +19,20 @@ #################################################################################################### import logging -import os from PyQt5 import QtWidgets from PyQt5.QtCore import Qt, QRegExp +from PyQt5.QtWidgets import QSizePolicy import pygit2 as git #################################################################################################### +from .LogTableModel import LogTableModel from CodeReview.GUI.Base.MainWindowBase import MainWindowBase from CodeReview.GUI.Widgets.IconLoader import IconLoader from CodeReview.GUI.Widgets.MessageBox import MessageBox -from .LogTableModel import LogTableModel +from CodeReview.Review import ReviewNote #################################################################################################### @@ -57,6 +58,8 @@ def __init__(self, parent=None): self._current_patch_index = None self._diff_window = None + self._review_note = None + self._init_ui() self._create_actions() self._create_toolbar() @@ -70,69 +73,79 @@ def _init_ui(self): # Table models are set in application - self._central_widget = QtWidgets.QWidget(self) - self.setCentralWidget(self._central_widget) + central_widget = QtWidgets.QWidget(self) + self.setCentralWidget(central_widget) - self._vertical_layout = QtWidgets.QVBoxLayout(self._central_widget) + top_vertical_layout = QtWidgets.QVBoxLayout(central_widget) self._message_box = MessageBox(self) - self._vertical_layout.addWidget(self._message_box) + top_vertical_layout.addWidget(self._message_box) self._branch_name = QtWidgets.QLineEdit(self) self._branch_name.setReadOnly(True) - self._vertical_layout.addWidget(self._branch_name) + top_vertical_layout.addWidget(self._branch_name) row = 0 - layout = QtWidgets.QGridLayout() - self._vertical_layout.addLayout(layout) + grid_layout = QtWidgets.QGridLayout() + top_vertical_layout.addLayout(grid_layout) label = QtWidgets.QLabel('Committer Filter') committer_filter = QtWidgets.QLineEdit() committer_filter.textChanged.connect(self._on_committer_filter_changed) for i, widget in enumerate((label, committer_filter)): - layout.addWidget(widget, row, i) + grid_layout.addWidget(widget, row, i) row += 1 horizontal_layout = QtWidgets.QHBoxLayout() - self._vertical_layout.addLayout(horizontal_layout) + top_vertical_layout.addLayout(horizontal_layout) label = QtWidgets.QLabel('Message Filter') message_filter = QtWidgets.QLineEdit() message_filter.textChanged.connect(self._on_message_filter_changed) for i, widget in enumerate((label, message_filter)): - layout.addWidget(widget, row, i) + grid_layout.addWidget(widget, row, i) row += 1 horizontal_layout = QtWidgets.QHBoxLayout() - self._vertical_layout.addLayout(horizontal_layout) + top_vertical_layout.addLayout(horizontal_layout) label = QtWidgets.QLabel('SHA Filter') sha_filter = QtWidgets.QLineEdit() sha_filter.textChanged.connect(self._on_sha_filter_changed) for i, widget in enumerate((label, sha_filter)): - layout.addWidget(widget, row, i) + grid_layout.addWidget(widget, row, i) row += 1 self._row_count = QtWidgets.QLabel('') - self._vertical_layout.addWidget(self._row_count) + top_vertical_layout.addWidget(self._row_count) splitter = QtWidgets.QSplitter() - self._vertical_layout.addWidget(splitter) + top_vertical_layout.addWidget(splitter) splitter.setOrientation(Qt.Vertical) self._log_table = QtWidgets.QTableView() splitter.addWidget(self._log_table) - widget = QtWidgets.QWidget() + bottom_widget = QtWidgets.QWidget() + splitter.addWidget(bottom_widget) + bottom_horizontal_layout = QtWidgets.QHBoxLayout() + bottom_widget.setLayout(bottom_horizontal_layout) + vertical_layout = QtWidgets.QVBoxLayout() - widget.setLayout(vertical_layout) - splitter.addWidget(widget) + bottom_horizontal_layout.addLayout(vertical_layout) self._commit_sha = QtWidgets.QLineEdit() self._commit_sha.setReadOnly(True) vertical_layout.addWidget(self._commit_sha) self._commit_table = QtWidgets.QTableView() vertical_layout.addWidget(self._commit_table) - # self._vertical_layout.addLayout(horizontal_layout) - # for widget in (self._message_box, splitter): - # self._vertical_layout.addWidget(widget) + vertical_layout = QtWidgets.QVBoxLayout() + bottom_horizontal_layout.addLayout(vertical_layout) + self._review_comment = QtWidgets.QTextEdit() + vertical_layout.addWidget(self._review_comment) + horizontal_layout = QtWidgets.QHBoxLayout() + vertical_layout.addLayout(horizontal_layout) + save_button = QtWidgets.QPushButton('Save') + save_button.clicked.connect(self._on_save_review) + horizontal_layout.addItem(QtWidgets.QSpacerItem(0, 10, QSizePolicy.Expanding)) + horizontal_layout.addWidget(save_button) table = self._log_table table.setSelectionMode(QtWidgets.QTableView.SingleSelection) @@ -313,6 +326,11 @@ def _update_working_tree_diff(self): def _update_commit_table(self, index=None): + if self._review_note is not None: + self._on_save_review() + self._review_note = None + self._review_comment.clear() + if index is not None: index = self._application.log_table_filter.mapToSource(index) index = index.row() @@ -331,9 +349,14 @@ def _update_commit_table(self, index=None): kwargs = dict(a=commit2, b=commit1) # Fixme: except IndexError: kwargs = dict(a=commit1) + self._review_note = self._application.review[sha] + if self._review_note is not None: + self._review_comment.setText(self._review_note.text) + else: + self._review_note = ReviewNote(sha) else: # working directory - self._commit_sha.setText('') + self._commit_sha.clear() self._current_revision = None if self._stagged_mode_action.isChecked(): # Changes between the index and your last commit @@ -515,4 +538,9 @@ def _on_log_table_filter_changed(self): log_table_filter = self._application.log_table_filter self._row_count.setText('{} commits'.format(log_table_filter.rowCount())) + ############################################## + def _on_save_review(self): + self._review_note.text = self._review_comment.toPlainText() + self._application.review.add(self._review_note) + self._application.review.save() diff --git a/CodeReview/Repository/Git.py b/CodeReview/Repository/Git.py index 062dc2b..74343b8 100644 --- a/CodeReview/Repository/Git.py +++ b/CodeReview/Repository/Git.py @@ -18,8 +18,8 @@ #################################################################################################### +from pathlib import Path import logging -import os import re import pygit2 as git @@ -39,24 +39,24 @@ class GitRepository: _logger = _module_logger.getChild('GitRepository') - INDEX_PATH = os.path.join('.git', 'index') - REFS_PATH = os.path.join('.git', 'refs', 'heads') + INDEX_PATH = Path('.git').joinpath('index') + REFS_PATH = Path('.git').joinpath('refs', 'heads') ############################################## def __init__(self, path): - path = os.path.realpath(path) + path = Path(path).absolute().resolve() try: - repository_path = git.discover_repository(path) + repository_path = git.discover_repository(str(path)) self._repository = git.Repository(repository_path) - workdir = self._repository.workdir - if os.path.isdir(path) and not path.endswith(os.sep): - path += os.sep - self._path_filter = path.replace(workdir, '') - self._logger.info('\nPath %s\nWork Dir: %s\nPath Filter: %s', - path, workdir, self._path_filter) + self._workdir = Path(self._repository.workdir) + # if path.is_dir() and not path_str.endswith(os.sep): + # path_str += os.sep + self._path_filter = path.relative_to(self._workdir) + template = '\nPath {}\nWork Dir: {}\nPath Filter: {}' + self._logger.info(template.format(path, self._workdir, self._path_filter)) except KeyError: raise RepositoryNotFound @@ -64,7 +64,7 @@ def __init__(self, path): @property def workdir(self): - return self._repository.workdir + return self._workdir @property def index(self): @@ -87,7 +87,7 @@ def branch_name(self): ############################################## def join_repository_path(self, path): - return os.path.join(self._repository.workdir, path) + return self._workdir.joinpath(path) ############################################## @@ -118,7 +118,7 @@ def commits(self): def diff(self, a=None, b=None, cached=False, path_filter=None): if path_filter is None: - path_filter = self._path_filter + path_filter = str(self._path_filter) patches = [] From fa8b09fb63ae654d0debd47f4f1280b24bb7dd4b Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Mon, 11 May 2020 23:05:21 +0200 Subject: [PATCH 12/69] fix path_filter --- CodeReview/Repository/Git.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CodeReview/Repository/Git.py b/CodeReview/Repository/Git.py index 74343b8..3f957b6 100644 --- a/CodeReview/Repository/Git.py +++ b/CodeReview/Repository/Git.py @@ -52,9 +52,9 @@ def __init__(self, path): repository_path = git.discover_repository(str(path)) self._repository = git.Repository(repository_path) self._workdir = Path(self._repository.workdir) - # if path.is_dir() and not path_str.endswith(os.sep): - # path_str += os.sep - self._path_filter = path.relative_to(self._workdir) + self._path_filter = str(path.relative_to(self._workdir)) + if self._path_filter == '.': + self._path_filter = '' template = '\nPath {}\nWork Dir: {}\nPath Filter: {}' self._logger.info(template.format(path, self._workdir, self._path_filter)) except KeyError: @@ -118,7 +118,7 @@ def commits(self): def diff(self, a=None, b=None, cached=False, path_filter=None): if path_filter is None: - path_filter = str(self._path_filter) + path_filter = self._path_filter patches = [] From 568856e118d5cdac7fd2485d8c9dc5329fc6bdee Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 00:01:39 +0200 Subject: [PATCH 13/69] add parent sha --- .../GUI/LogBrowser/LogBrowserMainWindow.py | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index c0f0bbc..a9dc566 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -81,13 +81,21 @@ def _init_ui(self): self._message_box = MessageBox(self) top_vertical_layout.addWidget(self._message_box) + horizontal_layout = QtWidgets.QHBoxLayout() + top_vertical_layout.addLayout(horizontal_layout) + + vertical_layout = QtWidgets.QVBoxLayout() + horizontal_layout.addLayout(vertical_layout) self._branch_name = QtWidgets.QLineEdit(self) self._branch_name.setReadOnly(True) - top_vertical_layout.addWidget(self._branch_name) + vertical_layout.addWidget(self._branch_name) + + self._row_count = QtWidgets.QLabel('') + vertical_layout.addWidget(self._row_count) row = 0 grid_layout = QtWidgets.QGridLayout() - top_vertical_layout.addLayout(grid_layout) + horizontal_layout.addLayout(grid_layout) label = QtWidgets.QLabel('Committer Filter') committer_filter = QtWidgets.QLineEdit() committer_filter.textChanged.connect(self._on_committer_filter_changed) @@ -113,9 +121,6 @@ def _init_ui(self): grid_layout.addWidget(widget, row, i) row += 1 - self._row_count = QtWidgets.QLabel('') - top_vertical_layout.addWidget(self._row_count) - splitter = QtWidgets.QSplitter() top_vertical_layout.addWidget(splitter) splitter.setOrientation(Qt.Vertical) @@ -133,6 +138,12 @@ def _init_ui(self): self._commit_sha = QtWidgets.QLineEdit() self._commit_sha.setReadOnly(True) vertical_layout.addWidget(self._commit_sha) + self._parent_labels = [] + for i in range(2): + parent = QtWidgets.QLineEdit() + parent.setReadOnly(True) + vertical_layout.addWidget(parent) + self._parent_labels.append(parent) self._commit_table = QtWidgets.QTableView() vertical_layout.addWidget(self._commit_table) @@ -324,8 +335,17 @@ def _update_working_tree_diff(self): ############################################## + def _reset_parent(self): + for parent in self._parent_labels: + parent.clear() + + ############################################## + def _update_commit_table(self, index=None): + self._commit_sha.clear() + self._reset_parent() + if self._review_note is not None: self._on_save_review() self._review_note = None @@ -343,7 +363,13 @@ def _update_commit_table(self, index=None): log_table_model = self._application.log_table_model commit1 = log_table_model[index] sha = commit1.hex - self._commit_sha.setText('SHA {} / {}'.format(sha[:8], sha)) + self._commit_sha.setText('Commit {} / {}'.format(sha[:8], sha)) + self._logger.info('Commit {}'.format(sha)) + self._logger.info('Parents: {}'.format(commit1.parent_ids)) + if len(commit1.parents) > len(self._parent_labels): + self.show_message('Fixme: More than 2 parents') + for commit, parent_label in zip(commit1.parents, self._parent_labels): + parent_label.setText('Parent {} {}'.format(commit.hex, commit.message)) try: commit2 = log_table_model[index +1] kwargs = dict(a=commit2, b=commit1) # Fixme: @@ -356,7 +382,6 @@ def _update_commit_table(self, index=None): self._review_note = ReviewNote(sha) else: # working directory - self._commit_sha.clear() self._current_revision = None if self._stagged_mode_action.isChecked(): # Changes between the index and your last commit From 2f74c544363b569471ef437ada8a4332ab3e9fb1 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 00:15:56 +0200 Subject: [PATCH 14/69] cleanup --- CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index a9dc566..29b4d44 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -362,24 +362,24 @@ def _update_commit_table(self, index=None): # log_table_model = self._log_table.model() log_table_model = self._application.log_table_model commit1 = log_table_model[index] + sha = commit1.hex self._commit_sha.setText('Commit {} / {}'.format(sha[:8], sha)) - self._logger.info('Commit {}'.format(sha)) - self._logger.info('Parents: {}'.format(commit1.parent_ids)) if len(commit1.parents) > len(self._parent_labels): self.show_message('Fixme: More than 2 parents') for commit, parent_label in zip(commit1.parents, self._parent_labels): parent_label.setText('Parent {} {}'.format(commit.hex, commit.message)) - try: - commit2 = log_table_model[index +1] - kwargs = dict(a=commit2, b=commit1) # Fixme: - except IndexError: - kwargs = dict(a=commit1) + self._review_note = self._application.review[sha] if self._review_note is not None: self._review_comment.setText(self._review_note.text) else: self._review_note = ReviewNote(sha) + try: + commit2 = log_table_model[index +1] + kwargs = dict(a=commit2, b=commit1) # Fixme: + except IndexError: + kwargs = dict(a=commit1) else: # working directory self._current_revision = None From e8c5978c0d59a2a26b24c0e970e021c24ed713ae Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 01:34:20 +0200 Subject: [PATCH 15/69] fix log table message ... --- CodeReview/GUI/LogBrowser/LogTableModel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogTableModel.py b/CodeReview/GUI/LogBrowser/LogTableModel.py index ac8dfc6..9e1157d 100644 --- a/CodeReview/GUI/LogBrowser/LogTableModel.py +++ b/CodeReview/GUI/LogBrowser/LogTableModel.py @@ -116,8 +116,8 @@ def _commit_data(self, i, commit): return ( self._number_of_rows - i -1, - tag_name + commit.message, - commit.hex, + tag_name + commit.message.strip(), + str(commit.hex), fromtimestamp(commit.commit_time).strftime('%Y-%m-%d %H:%M:%S'), '{} <{}>'.format(committer.name, committer.email), From d8ca49d08f8e72e0ddaddba50760c14dc0f347a0 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 01:34:41 +0200 Subject: [PATCH 16/69] implement go to parent and fix diff --- .../GUI/LogBrowser/LogBrowserMainWindow.py | 51 +++++++++++++------ CodeReview/GUI/LogBrowser/LogTableModel.py | 8 +++ CodeReview/Repository/Git.py | 10 ++++ 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index 29b4d44..954ecf5 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -44,6 +44,8 @@ class LogBrowserMainWindow(MainWindowBase): _logger = _module_logger.getChild('LogBrowserMainWindow') + SHA_SHORTCUT_LENGTH = 8 + ############################################## def __init__(self, parent=None): @@ -140,9 +142,14 @@ def _init_ui(self): vertical_layout.addWidget(self._commit_sha) self._parent_labels = [] for i in range(2): + horizontal_layout = QtWidgets.QHBoxLayout() + vertical_layout.addLayout(horizontal_layout) + button = QtWidgets.QPushButton('Go') + button.clicked.connect(lambda state, index=i: self._on_go_clicked(index)) + horizontal_layout.addWidget(button) parent = QtWidgets.QLineEdit() parent.setReadOnly(True) - vertical_layout.addWidget(parent) + horizontal_layout.addWidget(parent) self._parent_labels.append(parent) self._commit_table = QtWidgets.QTableView() vertical_layout.addWidget(self._commit_table) @@ -336,6 +343,7 @@ def _update_working_tree_diff(self): ############################################## def _reset_parent(self): + self._current_commit = None for parent in self._parent_labels: parent.clear() @@ -361,25 +369,26 @@ def _update_commit_table(self, index=None): self._current_revision = index # log_table_model = self._log_table.model() log_table_model = self._application.log_table_model - commit1 = log_table_model[index] + self._current_commit = log_table_model[index] - sha = commit1.hex - self._commit_sha.setText('Commit {} / {}'.format(sha[:8], sha)) - if len(commit1.parents) > len(self._parent_labels): + sha = self._current_commit.hex + self._commit_sha.setText('Commit: {} / {}'.format(sha[:self.SHA_SHORTCUT_LENGTH], sha)) + if len(self._current_commit.parents) > len(self._parent_labels): self.show_message('Fixme: More than 2 parents') - for commit, parent_label in zip(commit1.parents, self._parent_labels): - parent_label.setText('Parent {} {}'.format(commit.hex, commit.message)) + for commit, parent_label in zip(self._current_commit.parents, self._parent_labels): + parent_label.setText('Parent: {} ({})'.format(commit.hex[:self.SHA_SHORTCUT_LENGTH], commit.message)) + parent_label.setCursorPosition(0) self._review_note = self._application.review[sha] if self._review_note is not None: self._review_comment.setText(self._review_note.text) else: self._review_note = ReviewNote(sha) - try: - commit2 = log_table_model[index +1] - kwargs = dict(a=commit2, b=commit1) # Fixme: - except IndexError: - kwargs = dict(a=commit1) + # try: + commit_a = self._current_commit.parents[0] + kwargs = dict(a=commit_a, b=self._current_commit) # Fixme: + # except IndexError: + # kwargs = dict(a=self._current_commit) else: # working directory self._current_revision = None @@ -394,10 +403,7 @@ def _update_commit_table(self, index=None): kwargs = dict(a='HEAD') self._diff_kwargs = kwargs - self._logger.info('index {} kwargs {}'.format(index, kwargs)) - # Fixme: - if kwargs.get('a', None) is not None: - self._diff = self._application.repository.diff(**kwargs) + self._diff = self._application.repository.diff(**kwargs) commit_table_model = self._commit_table.model() commit_table_model.update(self._diff) @@ -569,3 +575,16 @@ def _on_save_review(self): self._review_note.text = self._review_comment.toPlainText() self._application.review.add(self._review_note) self._application.review.save() + + ############################################## + + def _on_go_clicked(self, parent_index): + if self._current_commit is not None: + try: + parent_commit_sha = str(self._current_commit.parent_ids[parent_index]) + index = self._application.log_table_model.find_commit(parent_commit_sha) + if index is not None: + self._logger.info('Found parent commit {} {}'.format(parent_index, parent_commit_sha)) + self._log_table.selectRow(index.row()) + except IndexError: + pass diff --git a/CodeReview/GUI/LogBrowser/LogTableModel.py b/CodeReview/GUI/LogBrowser/LogTableModel.py index 9e1157d..19563f5 100644 --- a/CodeReview/GUI/LogBrowser/LogTableModel.py +++ b/CodeReview/GUI/LogBrowser/LogTableModel.py @@ -131,6 +131,14 @@ def __getitem__(self, i): ############################################## + def find_commit(self, sha): + for i, row in enumerate(self._rows): + if row[2] == sha: + return self.createIndex(i, 0) + return None + + ############################################## + def data(self, index, role=Qt.DisplayRole): if not index.isValid(): # or not(0 <= index.row() < self._number_of_rows): diff --git a/CodeReview/Repository/Git.py b/CodeReview/Repository/Git.py index 3f957b6..1c4ef1c 100644 --- a/CodeReview/Repository/Git.py +++ b/CodeReview/Repository/Git.py @@ -117,6 +117,16 @@ def commits(self): def diff(self, a=None, b=None, cached=False, path_filter=None): + if isinstance(a, git.Commit): + a_str = a.hex + else: + a_str = str(a) + if isinstance(b, git.Commit): + b_str = b.hex + else: + b_str = str(b) + self._logger.info('{} {} {} {}'.format(a_str, b_str, cached, path_filter)) + if path_filter is None: path_filter = self._path_filter From aa3c8c11683821c7c0b4db004a1a97946fbac879 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 13:30:54 +0200 Subject: [PATCH 17/69] import tasks --- invoke.yaml | 1 + tasks/Makefile | 18 ++ tasks/__init__.py | 37 +++ tasks/clean.py | 38 +++ tasks/data/license-template.py | 19 ++ tasks/data/license-template.qml | 19 ++ tasks/doc.py | 62 ++++ tasks/get-material-icon | 184 +++++++++++ tasks/make-icon-png | 80 +++++ tasks/make-logo-png | 67 ++++ tasks/release.py | 99 ++++++ tasks/translate | 534 ++++++++++++++++++++++++++++++++ tasks/upload-to-pypi | 11 + 13 files changed, 1169 insertions(+) create mode 100644 invoke.yaml create mode 100644 tasks/Makefile create mode 100644 tasks/__init__.py create mode 100644 tasks/clean.py create mode 100644 tasks/data/license-template.py create mode 100644 tasks/data/license-template.qml create mode 100644 tasks/doc.py create mode 100755 tasks/get-material-icon create mode 100755 tasks/make-icon-png create mode 100755 tasks/make-logo-png create mode 100644 tasks/release.py create mode 100755 tasks/translate create mode 100755 tasks/upload-to-pypi diff --git a/invoke.yaml b/invoke.yaml new file mode 100644 index 0000000..5e908b5 --- /dev/null +++ b/invoke.yaml @@ -0,0 +1 @@ +Package: CodeReview diff --git a/tasks/Makefile b/tasks/Makefile new file mode 100644 index 0000000..45e869a --- /dev/null +++ b/tasks/Makefile @@ -0,0 +1,18 @@ +# -*- Makefile -*- + +#################################################################################################### + +all: code-review.rcc CodeReviewRessource.py + +#################################################################################################### + +%.rcc : %.qrc + rcc-qt5 -binary $< -o $@ + +CodeReviewRessource.py : code-review.qrc + pyrcc5 -o $@ $< + +#################################################################################################### + +clean: + rm *.py *.rcc diff --git a/tasks/__init__.py b/tasks/__init__.py new file mode 100644 index 0000000..ce165f0 --- /dev/null +++ b/tasks/__init__.py @@ -0,0 +1,37 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +# http://www.pyinvoke.org + +#################################################################################################### + +from invoke import task, Collection + # import sys + +#################################################################################################### + +from . import clean +from . import doc +from . import release + +ns = Collection() +ns.add_collection(Collection.from_module(clean)) +ns.add_collection(Collection.from_module(release)) +ns.add_collection(Collection.from_module(doc)) diff --git a/tasks/clean.py b/tasks/clean.py new file mode 100644 index 0000000..2ef1b71 --- /dev/null +++ b/tasks/clean.py @@ -0,0 +1,38 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +from invoke import task + +#################################################################################################### + +@task +def clean_flycheck(ctx): + with ctx.cd(ctx.Package): + ctx.run('find . -name "flycheck*.py" -exec rm {} \;') + +@task +def clean_emacs_backup(ctx): + ctx.run('find . -name "*~" -type f -exec rm -f {} \;') + +@task(clean_flycheck, clean_emacs_backup) +def clean(ctx): + pass diff --git a/tasks/data/license-template.py b/tasks/data/license-template.py new file mode 100644 index 0000000..1de9fa0 --- /dev/null +++ b/tasks/data/license-template.py @@ -0,0 +1,19 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### diff --git a/tasks/data/license-template.qml b/tasks/data/license-template.qml new file mode 100644 index 0000000..63f63df --- /dev/null +++ b/tasks/data/license-template.qml @@ -0,0 +1,19 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ diff --git a/tasks/doc.py b/tasks/doc.py new file mode 100644 index 0000000..9f92dca --- /dev/null +++ b/tasks/doc.py @@ -0,0 +1,62 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +from invoke import task + # import sys + +#################################################################################################### + +from .clean import clean_flycheck as clean_flycheck + +#################################################################################################### + +@task +def clean_api(ctx): + ctx.run('rm -rf doc/sphinx/source/api') + +#################################################################################################### + +@task(clean_flycheck, clean_api) +def make_api(ctx): + print('\nGenerate RST API files') + ctx.run('pyterate-rst-api {0.Package}'.format(ctx)) + print('\nRun Sphinx') + with ctx.cd('doc/sphinx/'): + ctx.run('./make-html') #--clean + +#################################################################################################### + +@task() +def make_readme(ctx): + from setup_data import long_description + with open('README.rst', 'w') as fh: + fh.write(long_description) + # import subprocess + # subprocess.call(('rst2html', 'README.rst', 'README.html')) + ctx.run('rst2html README.rst README.html') + +#################################################################################################### + +@task +def update_authors(ctx): + # Keep authors in the order of appearance and use awk to filter out dupes + ctx.run("git log --format='- %aN <%aE>' --reverse | awk '!x[$0]++' > AUTHORS") diff --git a/tasks/get-material-icon b/tasks/get-material-icon new file mode 100755 index 0000000..61673a4 --- /dev/null +++ b/tasks/get-material-icon @@ -0,0 +1,184 @@ +#! /usr/bin/env python3 + +#################################################################################################### + +# https://material.io/tools/icons/static/icons/baseline-save-24px.svg +# https://material.io/tools/icons/static/icons/baseline-save-black-36.zip + +# https://material.io/tools/icons/static/icons/outline-save-black-36.zip +# https://material.io/tools/icons/static/icons/round-save-black-36.zip +# https://material.io/tools/icons/static/icons/sharp-save-black-36.zip +# https://material.io/tools/icons/static/icons/twotone-save-black-36.zip + +# https://material.io/tools/icons/static/icons/baseline-save-white-36.zip + +# 1x/baseline_save_black_18dp.png: PNG image data, 18 x 18, 8-bit gray+alpha, non-interlaced +# 1x/baseline_save_black_24dp.png: PNG image data, 24 x 24, 8-bit gray+alpha, non-interlaced +# 1x/baseline_save_black_36dp.png: PNG image data, 36 x 36, 8-bit gray+alpha, non-interlaced +# 1x/baseline_save_black_48dp.png: PNG image data, 48 x 48, 8-bit gray+alpha, non-interlaced + +# 2x/baseline_save_black_18dp.png: PNG image data, 36 x 36, 8-bit gray+alpha, non-interlaced +# 2x/baseline_save_black_24dp.png: PNG image data, 48 x 48, 8-bit gray+alpha, non-interlaced +# 2x/baseline_save_black_36dp.png: PNG image data, 72 x 72, 8-bit gray+alpha, non-interlaced +# 2x/baseline_save_black_48dp.png: PNG image data, 96 x 96, 8-bit gray+alpha, non-interlaced + +#################################################################################################### + +from pathlib import Path +from zipfile import ZipFile +import argparse +import os +import shutil +import tempfile +import urllib3 + +#################################################################################################### + +parser = argparse.ArgumentParser(description='Fetch material icon.') + +parser.add_argument( + 'src_name', metavar='NAME', + help='icon name', +) + +parser.add_argument( + '--dst-name', + default=None, + help='dst name, default is same name', +) + +parser.add_argument( + '--style', + default='baseline', + help='style: [baseline], outline, round, twotone, sharp', +) + +parser.add_argument( + '--color', + default='black', + help='color: [black], white', +) + +args = parser.parse_args() + +#################################################################################################### + +urllib3.disable_warnings() + +#################################################################################################### + +class MaterialIconFetcher: + + SCALE = (1, 2) + DP = (18, 24, 36, 48) + + ############################################## + + def __init__(self, icons_path, theme): + + self._icons_path = Path(str(icons_path)).resolve() + self._theme = str(theme) + self._theme_path = self._icons_path.joinpath(self._theme) + + if not self._icons_path.exists(): + os.mkdir(self._icons_path) + if not self._theme_path.exists(): + os.mkdir(self._theme_path) + + self._http = urllib3.PoolManager() + + # with tempfile.TemporaryDirectory() as tmp_directory: + self._tmp_directory = tempfile.TemporaryDirectory() + self._tmp_directory_path = Path(self._tmp_directory.name) + print('tmp_directory', self._tmp_directory_path) + + ############################################## + + def _fetch_ressource(self, url): + print('Fetch', url, '...') + request = self._http.request('GET', url) + return request.data + + ############################################## + + def _fetch_png_icon(self, **kwargs): + + # https://material.io/tools/icons/static/icons/baseline-save-black-36.zip + root = 'https://material.io/tools/icons/static/icons/' + url_pattern = '{style}-{name}-{color}-{dp}.zip' + filename = url_pattern.format(**kwargs) + url = root + filename + + data = self._fetch_ressource(url) + + return filename, data + + ############################################## + + def _extract_png_archive(self, **kwargs): + + filename, data = self._fetch_png_icon(**kwargs) + zip_path = self._tmp_directory_path.joinpath(filename) + with open(zip_path, 'wb') as fh: + fh.write(data) + with ZipFile(zip_path, 'r') as zip_archive: + zip_archive.extractall(self._tmp_directory_path) + + ############################################## + + def fetch_icon(self, src_name, dst_name, style, color): + + kwargs = dict(src_name=src_name, dst_name=dst_name, style=style, color=color) + + for dp in self.DP: + self._extract_png_archive(name=src_name, dp=dp, **kwargs) + + print() + for scale in self.SCALE: + for dp in self.DP: + dst_kwargs = dict(kwargs) + dst_kwargs.update(dict(scale=scale, dp=dp)) + + # 1x/baseline_save_black_18dp.png + filename_pattern = '{style}_{src_name}_{color}_{dp}dp.png' + src_path = self._tmp_directory_path.joinpath( + '{scale}x'.format(**dst_kwargs), + filename_pattern.format(**dst_kwargs), + ) + + if scale > 1: + size_directory = '{dp}x{dp}@{scale}'.format(**dst_kwargs) + else: + size_directory = '{dp}x{dp}'.format(**dst_kwargs) + size_path = self._theme_path.joinpath(size_directory) + if not size_path.exists(): + os.mkdir(size_path) + filename_pattern = '{dst_name}-{color}.png' + filename = filename_pattern.format(**dst_kwargs) + dst_path = size_path.joinpath(filename) + + # print('Copy', src_path, dst_path) + shutil.copyfile(src_path, dst_path) + + rcc_pattern = 'icons/{}/{}/{}' + rcc_line = rcc_pattern.format(self._theme, size_directory, filename) + if dp != 36: + rcc_line = ''.format(rcc_line) + rcc_line = ' '*8 + rcc_line + print(rcc_line) + +#################################################################################################### + +root_path = Path(__file__).resolve().parents[1] +icons_path = root_path.joinpath('share', 'icons') +theme = 'material' +print('Icons path:', icons_path, theme) + +fetcher = MaterialIconFetcher(icons_path, theme) + +fetcher.fetch_icon( + args.src_name, + args.dst_name or args.src_name.replace('_', '-'), + args.style, + args.color, +) diff --git a/tasks/make-icon-png b/tasks/make-icon-png new file mode 100755 index 0000000..b982a8d --- /dev/null +++ b/tasks/make-icon-png @@ -0,0 +1,80 @@ +#! /usr/bin/env python3 + +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +from pathlib import Path +import argparse +import subprocess + +#################################################################################################### + +parser = argparse.ArgumentParser(description='Generate PNG files from a SVG.') +parser.add_argument( + 'svg_path', metavar='file.svg', + help='SVG PATH', +) +args = parser.parse_args() + +#################################################################################################### + +theme = 'material' +DP = (36, 48) +SCALE = (1, 2) + +svg_path = Path(args.svg_path).resolve() +theme_path = svg_path.parents[1].joinpath(theme) +print('Theme path:', theme_path) + +#################################################################################################### + +inkscape_options = [ + '--export-area-page', + '--export-background=white', + '--export-background-opacity=0', +] + +#################################################################################################### + +def run_inkscape(svg_path, dp, scale): + filename = svg_path.name.replace('.svg', '.png') + if scale > 1: + size_directory = '{dp}x{dp}@{scale}'.format(dp=dp, scale=scale) + else: + size_directory = '{dp}x{dp}'.format(dp=dp) + png_path = theme_path.joinpath(size_directory, filename) + command = ( + 'inkscape', + *inkscape_options, + '--export-png={}'.format(png_path), + # --export-width= + '--export-height={}'.format(dp*scale), + str(svg_path), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + +#################################################################################################### + +for dp in DP: + for scale in SCALE: + run_inkscape(svg_path, dp, scale) diff --git a/tasks/make-logo-png b/tasks/make-logo-png new file mode 100755 index 0000000..d5dd866 --- /dev/null +++ b/tasks/make-logo-png @@ -0,0 +1,67 @@ +#! /usr/bin/env python3 + +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +from pathlib import Path +import argparse +import subprocess + +#################################################################################################### + +parser = argparse.ArgumentParser(description='Generate PNG files from a SVG.') +parser.add_argument( + 'svg_path', metavar='file.svg', + help='SVG PATH', +) +args = parser.parse_args() + +svg_path = Path(args.svg_path).resolve() + +#################################################################################################### + +sizes = (32, 64, 96, 128, 256, 512) + +inkscape_options = [ + '--export-area-page', + '--export-background=white', + '--export-background-opacity=0', +] + +#################################################################################################### + +def run_inkscape(svg_path, size): + command = ( + 'inkscape', + *inkscape_options, + '--export-png=logo-{}.png'.format(size), + # --export-width= + '--export-height={}'.format(size), + str(svg_path), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + +#################################################################################################### + +for size in sizes: + run_inkscape(svg_path, size) diff --git a/tasks/release.py b/tasks/release.py new file mode 100644 index 0000000..3eaeb1b --- /dev/null +++ b/tasks/release.py @@ -0,0 +1,99 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +from invoke import task + +#################################################################################################### + +STANDARD_PACKAGES = ( + 'argparse', + 'atexit', + 'datetime', + 'hashlib', + 'importlib', + 'json', + 'logging', + 'operator', + 'os', + 'pathlib', + 'shutil', + 'signal', + 'stat', + 'subprocess', + 'sys', + 'time', + 'traceback', +) + +@task() +def show_import(ctx): + package = ctx.Package + with ctx.cd(package): + result = ctx.run("grep -r -h -E '^(import|from) [a-zA-Z]' . | sort | uniq", hide='out') + imports = set() + for line in result.stdout.split('\n'): + if line.startswith('from'): + position = line.find('import') + line = line[5:position] + elif line.startswith('import'): + line = line[7:] + # print('|{}|'.format(line)) + position = line.find('.') + if position != -1: + line = line[:position] + line = line.strip() + if line: + imports.add(line) + imports -= set(STANDARD_PACKAGES) + imports -= set((package,)) + for item in sorted(imports): + print(item) + +@task +def find_package(ctx, name): + ctx.run('pip freeze | grep -i {}'.format(name)) + +#################################################################################################### + +@task() +def update_git_sha(ctx): + result = ctx.run('git describe --tags --abbrev=0 --always', hide='out') + sha = result.stdout.strip() + filename = Path(ctx.Package, '__init__.py') + with open(str(filename) + '.in', 'r') as fh: + lines = fh.readlines() + with open(filename, 'w') as fh: + for line in lines: + if '@' in line: + line = line.replace('@GIT_SHA@', sha) + fh.write(line) + +#################################################################################################### + +def show_python_site(ctx): + ctx.run('python3 -m site') + +@task(update_git_sha) +def build(ctx): + ctx.run('python3 setup.py build') + +@task(build) +def install(ctx): + ctx.run('python3 setup.py install') diff --git a/tasks/translate b/tasks/translate new file mode 100755 index 0000000..d1f0d4c --- /dev/null +++ b/tasks/translate @@ -0,0 +1,534 @@ +#! /usr/bin/env python3 + +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +"""Program to manage and translate Qt translation files. + +It features: + +* lupdate, linguist, lrelease +* conversion to/from .po file +* poedit +* translation using Google Translation + +Actions are run in the right order. + +""" + +#################################################################################################### + +from pathlib import Path +import argparse +import os +import subprocess + +from googletrans import Translator +# https://py-googletrans.readthedocs.io/en/latest/ + +#################################################################################################### + +DEFAULT_LANGUAGES = ( + 'fr_FR', +) + +# Fixme: project dependent +source_path = Path(__file__).resolve().parents[1] +qml_path = source_path.joinpath('qml') +py_path = source_path.joinpath('CodeReview') +translation_path = source_path.joinpath('share', 'translations') +base_name = 'code-review' + +#################################################################################################### + +parser = argparse.ArgumentParser(description='Translate Qt Application.') + +parser.add_argument( + '--language', + default=None, + help='languages, default {}'.format(DEFAULT_LANGUAGES), +) + +parser.add_argument( + '--update', + default=False, + action='store_true', + help='run lupdate', +) + +parser.add_argument( + '--translate', + default=False, + action='store_true', + help='translate using Google Translate', +) + +parser.add_argument( + '--convert-to-po', + default=False, + action='store_true', + help='convert to .po file', +) + +parser.add_argument( + '--convert-from-po', + default=False, + action='store_true', + help='convert from .po file', +) + +parser.add_argument( + '--poedit', + default=False, + action='store_true', + help='run poedit', +) + +parser.add_argument( + '--linguist', + default=False, + action='store_true', + help='run linguist', +) + +parser.add_argument( + '--release', + default=False, + action='store_true', + help='run lrelease', +) + +args = parser.parse_args() + +#################################################################################################### + +def _clean_path(path): + return Path(str(path)).resolve() + +#################################################################################################### + +class TanslationManager: + + ############################################## + + def __init__(self, translation_path, base_name, py_path, qml_path): + + self._translation_path = _clean_path(translation_path) + self._base_name = base_name + self._py_path = _clean_path(py_path) + self._qml_path = _clean_path(qml_path) + + if not self._translation_path.exists(): + os.mkdir(self._translation_path) + + ############################################## + + def ts_path(self, scope, language): + if scope: + filename = '{}.{}.{}.ts'.format(self._base_name, scope, language) + else: + filename = '{}.{}.ts'.format(self._base_name, language) + return self._translation_path.joinpath(filename) + + def po_path(self, scope, language): + filename = '{}.{}.{}.po'.format(self._base_name, scope, language) + return self._translation_path.joinpath(filename) + + ############################################## + + def run_lupdate(self, language): + + # Usage: + # lupdate [options] [project-file]... + # lupdate [options] [source-file|path|@lst-file]... -ts ts-files|@lst-file + # + # lupdate is part of Qt's Linguist tool chain. It extracts translatable + # messages from Qt UI files, C++, Java and JavaScript/QtScript source code. + # Extracted messages are stored in textual translation source files (typically + # Qt TS XML). New and modified messages can be merged into existing TS files. + # + # Options: + # -help Display this information and exit. + # -no-obsolete + # Drop all obsolete and vanished strings. + # -extensions [,]... + # Process files with the given extensions only. + # The extension list must be separated with commas, not with whitespace. + # Default: 'java,jui,ui,c,c++,cc,cpp,cxx,ch,h,h++,hh,hpp,hxx,js,qs,qml,qrc'. + # -pluralonly + # Only include plural form messages. + # -silent + # Do not explain what is being done. + # -no-sort + # Do not sort contexts in TS files. + # -no-recursive + # Do not recursively scan the following directories. + # -recursive + # Recursively scan the following directories (default). + # -I or -I + # Additional location to look for include files. + # May be specified multiple times. + # -locations {absolute|relative|none} + # Specify/override how source code references are saved in TS files. + # Guessed from existing TS files if not specified. + # Default is absolute for new files. + # -no-ui-lines + # Do not record line numbers in references to UI files. + # -disable-heuristic {sametext|similartext|number} + # Disable the named merge heuristic. Can be specified multiple times. + # -pro + # Name of a .pro file. Useful for files with .pro file syntax but + # different file suffix. Projects are recursed into and merged. + # -pro-out + # Virtual output directory for processing subsequent .pro files. + # -pro-debug + # Trace processing .pro files. Specify twice for more verbosity. + # -source-language [_] + # Specify the language of the source strings for new files. + # Defaults to POSIX if not specified. + # -target-language [_] + # Specify the language of the translations for new files. + # Guessed from the file name if not specified. + # -tr-function-alias {+=,=}[,{+=,=}]... + # With +=, recognize as an alternative spelling of . + # With =, recognize as the only spelling of . + # Available s (with their currently defined aliases) are: + # Q_DECLARE_TR_FUNCTIONS (=Q_DECLARE_TR_FUNCTIONS) + # QT_TR_N_NOOP (=QT_TR_N_NOOP) + # QT_TRID_N_NOOP (=QT_TRID_N_NOOP) + # QT_TRANSLATE_N_NOOP (=QT_TRANSLATE_N_NOOP) + # QT_TRANSLATE_N_NOOP3 (=QT_TRANSLATE_N_NOOP3) + # QT_TR_NOOP (=QT_TR_NOOP) + # QT_TRID_NOOP (=QT_TRID_NOOP) + # QT_TRANSLATE_NOOP (=QT_TRANSLATE_NOOP) + # QT_TRANSLATE_NOOP3 (=QT_TRANSLATE_NOOP3) + # QT_TR_NOOP_UTF8 (=QT_TR_NOOP_UTF8) + # QT_TRANSLATE_NOOP_UTF8 (=QT_TRANSLATE_NOOP_UTF8) + # QT_TRANSLATE_NOOP3_UTF8 (=QT_TRANSLATE_NOOP3_UTF8) + # findMessage (=findMessage) + # qtTrId (=qtTrId) + # tr (=tr) + # trUtf8 (=trUtf8) + # translate (=translate) + # qsTr (=qsTr) + # qsTrId (=qsTrId) + # qsTranslate (=qsTranslate) + # -ts ... + # Specify the output file(s). This will override the TRANSLATIONS. + # -version + # Display the version of lupdate and exit. + # @lst-file + # Read additional file names (one per line) or includepaths (one per + # line, and prefixed with -I) from lst-file. + + command = ( + 'lupdate-qt5', + '-extensions', 'qml,js', + '-source-language', 'en_GB', + '-target-language', language, + str(self._qml_path), + '-ts', str(self.ts_path('qml', language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + py_filenames = [] + for root, _, filenames in os.walk(self._py_path): + root = Path(root) + for filename in filenames: + filename = Path(filename) + if filename.suffix == '.py': + filename = str(root.joinpath(filename)) + py_filenames.append(filename) + + command = ( + 'pylupdate5', + '-verbose', + *py_filenames, + '-ts', str(self.ts_path('py', language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + # Fixme: pylupdate5 don't support qml and folder !!! + + # print('Fix obsolete') + # with open(ts_path, 'r') as fh: + # content = fh.readlines() + # with open(ts_path, 'w') as fh: + # for line in content: + # line = line.replace('type="obsolete"', '') + # line = line.replace('', '') + # fh.write(line) + + ############################################## + + def run_linguist(self, language): + + for scope in ('qml', 'py'): + command = ( + 'linguist-qt5', + str(self.ts_path(scope, language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + ############################################## + + def run_poedit(self, language): + + command = ( + 'poedit', + str(self.po_path(language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + ############################################## + + def run_lrelease(self, language): + + # Usage: + # lrelease [options] project-file + # lrelease [options] ts-files [-qm qm-file] + # + # lrelease is part of Qt's Linguist tool chain. It can be used as a + # stand-alone tool to convert XML-based translations files in the TS + # format into the 'compiled' QM format used by QTranslator objects. + # + # Options: + # -help Display this information and exit + # -idbased + # Use IDs instead of source strings for message keying + # -compress + # Compress the QM files + # -nounfinished + # Do not include unfinished translations + # -removeidentical + # If the translated text is the same as + # the source text, do not include the message + # -markuntranslated + # If a message has no real translation, use the source text + # prefixed with the given string instead + # -silent + # Do not explain what is being done + # -version + # Display the version of lrelease and exit + + command = ( + 'lconvert-qt5', + '-i', + str(self.ts_path('qml', language)), + str(self.ts_path('py', language)), + '-o', str(self.ts_path(None, language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + command = ( + 'lrelease-qt5', + str(self.ts_path(None, language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + ############################################## + + def run_lconvert(self, language, format): + + # Usage: + # lconvert [options] [...] + # + # lconvert is part of Qt's Linguist tool chain. It can be used as a + # stand-alone tool to convert and filter translation data files. + # The following file formats are supported: + # + # qm - Traductions Qt compilées + # pot - Fichiers de modèle de localisation GNU Gettext + # qph - Qt Linguist "livre de phrases" + # ts - Sources de traduction Qt + # po - Fichiers de localisation GNU Gettext + # xlf - Fichiers de localisation XLIFF + # + # If multiple input files are specified, they are merged with + # translations from later files taking precedence. + # + # Options: + # -h + # -help Display this information and exit. + # + # -i + # -input-file + # Specify input file. Use if might start with a dash. + # This option can be used several times to merge inputs. + # May be '-' (standard input) for use in a pipe. + # + # -o + # -output-file + # Specify output file. Default is '-' (standard output). + # + # -if + # -input-format + # Specify input format for subsequent s. + # The format is auto-detected from the file name and defaults to 'ts'. + # + # -of + # -output-format + # Specify output format. See -if. + # + # -drop-tags + # Drop named extra tags when writing TS or XLIFF files. + # May be specified repeatedly. + # + # -drop-translations + # Drop existing translations and reset the status to 'unfinished'. + # Note: this implies --no-obsolete. + # + # -source-language [_] + # Specify/override the language of the source strings. Defaults to + # POSIX if not specified and the file does not name it yet. + # + # -target-language [_] + # Specify/override the language of the translation. + # The target language is guessed from the file name if this option + # is not specified and the file contents name no language yet. + # + # -no-obsolete + # Drop obsolete messages. + # + # -no-finished + # Drop finished messages. + # + # -no-untranslated + # Drop untranslated messages. + # + # -sort-contexts + # Sort contexts in output TS file alphabetically. + # + # -locations {absolute|relative|none} + # Override how source code references are saved in TS files. + # Default is absolute. + # + # -no-ui-lines + # Drop line numbers from references to UI files. + # + # -verbose + # be a bit more verbose + # + # Long options can be specified with only one leading dash, too. + # + # Return value: + # 0 on success + # 1 on command line parse failures + # 2 on read failures + # 3 on write failures + + is_po = format == 'po' + + for scope in ('qml', 'py'): + command = ( + 'lconvert-qt5', + '-i' if is_po else '-o', str(self.ts_path(scope, language)), + '-o' if is_po else '-i', str(self.po_path(language)), + ) + print('>', ' '.join(command)) + subprocess.check_call(command) + + ############################################## + + def translate(self, language): + for scope in ('qml', 'py'): + self.translate_scope(scope, language) + + ############################################## + + def translate_scope(self, scope, language): + + print('Translate {} {} ...'.format(scope, language)) + + language_code = language[:2] + if language_code == 'en': + return + + translator = Translator( + service_urls=[ + 'translate.google.' + language_code, + 'translate.google.com', + ], + ) + + def translate_string(source): + return translator.translate(source, src='en', dest=language_code).text + + ts_path = self.ts_path(scope, language) + with open(ts_path, 'r') as fh: + lines = list(fh.readlines()) + + with open(ts_path, 'w') as fh: + source = None + for line in lines: + striped_line = line.strip() + if striped_line.startswith(''): + source = striped_line[striped_line.find('>')+1:striped_line.rfind('<')] + elif striped_line.startswith(''): + old_translation = striped_line[striped_line.find('>')+1:striped_line.rfind('<')] + if not old_translation: + translation = translate_string(source) + print() + print(source) + print(translation) + line = line.replace(' type="unfinished">', '>' + translation) + fh.write(line) + +#################################################################################################### + +manager = TanslationManager( + translation_path, + base_name, + py_path, + qml_path, +) + +if not args.language: + languages = DEFAULT_LANGUAGES +else: + languages = [x for x in [x.strip() for x in args.languages.split(',')] if x] + +if args.update: + for language in languages: + manager.run_lupdate(language) +if args.translate: + for language in languages: + manager.translate(language) +if args.convert_to_po: + for language in languages: + manager.run_lconvert(language, 'po') +if args.poedit: + for language in languages: + manager.run_poedit(language) +if args.convert_from_po: + for language in languages: + manager.run_lconvert(language, 'ts') +if args.linguist: + for language in languages: + manager.run_linguist(language) +if args.release: + for language in languages: + manager.run_lrelease(language) diff --git a/tasks/upload-to-pypi b/tasks/upload-to-pypi new file mode 100755 index 0000000..36398ab --- /dev/null +++ b/tasks/upload-to-pypi @@ -0,0 +1,11 @@ +#! /bin/bash + +# bdist +# python setup.py check --verbose --metadata --restructuredtext --strict && \ +# python setup.py register sdist upload + +python setup.py bdist_wheel + +twine register dist/*whl +gpg --detach-sign -a dist/*whl +twine upload dist/* From 94537932bc4713cb000b42f2faf03b9f9ed9d4c4 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 13:31:42 +0200 Subject: [PATCH 18/69] import QtShim --- QtShim/QtConfig.py | 745 ++++++++++++++++++++++++++++++++++++++ QtShim/Wrapper.py | 34 ++ QtShim/__init__.py | 389 ++++++++++++++++++++ QtShim/not-implemented.py | 116 ++++++ 4 files changed, 1284 insertions(+) create mode 100644 QtShim/QtConfig.py create mode 100644 QtShim/Wrapper.py create mode 100644 QtShim/__init__.py create mode 100644 QtShim/not-implemented.py diff --git a/QtShim/QtConfig.py b/QtShim/QtConfig.py new file mode 100644 index 0000000..e1b09db --- /dev/null +++ b/QtShim/QtConfig.py @@ -0,0 +1,745 @@ +#################################################################################################### + +"""Common members of all bindings + +This is where each member of Qt.py is explicitly defined. +It is based on a 'lowest common denominator' of all bindings; +including members found in each of the 4 bindings. + +The '_common_members' dictionary is generated using the +build_membership.sh script. + +""" + +_common_members = { + 'QtCore': [ + 'QAbstractAnimation', + 'QAbstractEventDispatcher', + 'QAbstractItemModel', + 'QAbstractListModel', + 'QAbstractState', + 'QAbstractTableModel', + 'QAbstractTransition', + 'QAnimationGroup', + 'QBasicTimer', + 'QBitArray', + 'QBuffer', + 'QByteArray', + 'QByteArrayMatcher', + 'QChildEvent', + 'QCoreApplication', + 'QCryptographicHash', + 'QDataStream', + 'QDate', + 'QDateTime', + 'QDir', + 'QDirIterator', + 'QDynamicPropertyChangeEvent', + 'QEasingCurve', + 'QElapsedTimer', + 'QEvent', + 'QEventLoop', + 'QEventTransition', + 'QFile', + 'QFileInfo', + 'QFileSystemWatcher', + 'QFinalState', + 'QGenericArgument', + 'QGenericReturnArgument', + 'QHistoryState', + 'QItemSelectionRange', + 'QIODevice', + 'QLibraryInfo', + 'QLine', + 'QLineF', + 'QLocale', + 'QMargins', + 'QMetaClassInfo', + 'QMetaEnum', + 'QMetaMethod', + 'QMetaObject', + 'QMetaProperty', + 'QMimeData', + 'QModelIndex', + 'QMutex', + 'QMutexLocker', + 'QObject', + 'QParallelAnimationGroup', + 'QPauseAnimation', + 'QPersistentModelIndex', + 'QPluginLoader', + 'QPoint', + 'QPointF', + 'QProcess', + 'QProcessEnvironment', + 'QPropertyAnimation', + 'QReadLocker', + 'QReadWriteLock', + 'QRect', + 'QRectF', + 'QRegExp', + 'QResource', + 'QRunnable', + 'QSemaphore', + 'QSequentialAnimationGroup', + 'QSettings', + 'QSignalMapper', + 'QSignalTransition', + 'QSize', + 'QSizeF', + 'QSocketNotifier', + 'QState', + 'QStateMachine', + 'QSysInfo', + 'QSystemSemaphore', + 'QT_TRANSLATE_NOOP', + 'QT_TR_NOOP', + 'QT_TR_NOOP_UTF8', + 'QTemporaryFile', + 'QTextBoundaryFinder', + 'QTextCodec', + 'QTextDecoder', + 'QTextEncoder', + 'QTextStream', + 'QTextStreamManipulator', + 'QThread', + 'QThreadPool', + 'QTime', + 'QTimeLine', + 'QTimer', + 'QTimerEvent', + 'QTranslator', + 'QUrl', + 'QVariantAnimation', + 'QWaitCondition', + 'QWriteLocker', + 'QXmlStreamAttribute', + 'QXmlStreamAttributes', + 'QXmlStreamEntityDeclaration', + 'QXmlStreamEntityResolver', + 'QXmlStreamNamespaceDeclaration', + 'QXmlStreamNotationDeclaration', + 'QXmlStreamReader', + 'QXmlStreamWriter', + 'Qt', + 'QtCriticalMsg', + 'QtDebugMsg', + 'QtFatalMsg', + 'QtMsgType', + 'QtSystemMsg', + 'QtWarningMsg', + 'qAbs', + 'qAddPostRoutine', + 'qChecksum', + 'qCritical', + 'qDebug', + 'qFatal', + 'qFuzzyCompare', + 'qIsFinite', + 'qIsInf', + 'qIsNaN', + 'qIsNull', + 'qRegisterResourceData', + 'qUnregisterResourceData', + 'qVersion', + 'qWarning', + 'qrand', + 'qsrand' + ], + 'QtGui': [ + 'QAbstractTextDocumentLayout', + 'QActionEvent', + 'QBitmap', + 'QBrush', + 'QClipboard', + 'QCloseEvent', + 'QColor', + 'QConicalGradient', + 'QContextMenuEvent', + 'QCursor', + 'QDesktopServices', + 'QDoubleValidator', + 'QDrag', + 'QDragEnterEvent', + 'QDragLeaveEvent', + 'QDragMoveEvent', + 'QDropEvent', + 'QFileOpenEvent', + 'QFocusEvent', + 'QFont', + 'QFontDatabase', + 'QFontInfo', + 'QFontMetrics', + 'QFontMetricsF', + 'QGradient', + 'QGuiApplication', + 'QHelpEvent', + 'QHideEvent', + 'QHoverEvent', + 'QIcon', + 'QIconDragEvent', + 'QIconEngine', + 'QImage', + 'QImageIOHandler', + 'QImageReader', + 'QImageWriter', + 'QInputEvent', + 'QInputMethodEvent', + 'QIntValidator', + 'QKeyEvent', + 'QKeySequence', + 'QLinearGradient', + 'QMatrix2x2', + 'QMatrix2x3', + 'QMatrix2x4', + 'QMatrix3x2', + 'QMatrix3x3', + 'QMatrix3x4', + 'QMatrix4x2', + 'QMatrix4x3', + 'QMatrix4x4', + 'QMouseEvent', + 'QMoveEvent', + 'QMovie', + 'QPaintDevice', + 'QPaintEngine', + 'QPaintEngineState', + 'QPaintEvent', + 'QPainter', + 'QPainterPath', + 'QPainterPathStroker', + 'QPalette', + 'QPen', + 'QPicture', + 'QPictureIO', + 'QPixmap', + 'QPixmapCache', + 'QPolygon', + 'QPolygonF', + 'QQuaternion', + 'QRadialGradient', + 'QRegExpValidator', + 'QRegion', + 'QResizeEvent', + 'QSessionManager', + 'QShortcutEvent', + 'QShowEvent', + 'QStandardItem', + 'QStandardItemModel', + 'QStatusTipEvent', + 'QSyntaxHighlighter', + 'QTabletEvent', + 'QTextBlock', + 'QTextBlockFormat', + 'QTextBlockGroup', + 'QTextBlockUserData', + 'QTextCharFormat', + 'QTextCursor', + 'QTextDocument', + 'QTextDocumentFragment', + 'QTextFormat', + 'QTextFragment', + 'QTextFrame', + 'QTextFrameFormat', + 'QTextImageFormat', + 'QTextInlineObject', + 'QTextItem', + 'QTextLayout', + 'QTextLength', + 'QTextLine', + 'QTextList', + 'QTextListFormat', + 'QTextObject', + 'QTextObjectInterface', + 'QTextOption', + 'QTextTable', + 'QTextTableCell', + 'QTextTableCellFormat', + 'QTextTableFormat', + 'QTouchEvent', + 'QTransform', + 'QValidator', + 'QVector2D', + 'QVector3D', + 'QVector4D', + 'QWhatsThisClickedEvent', + 'QWheelEvent', + 'QWindowStateChangeEvent', + 'qAlpha', + 'qBlue', + 'qGray', + 'qGreen', + 'qIsGray', + 'qRed', + 'qRgb', + 'qRgba' + ], + # 'QtHelp': [ + # 'QHelpContentItem', + # 'QHelpContentModel', + # 'QHelpContentWidget', + # 'QHelpEngine', + # 'QHelpEngineCore', + # 'QHelpIndexModel', + # 'QHelpIndexWidget', + # 'QHelpSearchEngine', + # 'QHelpSearchQuery', + # 'QHelpSearchQueryWidget', + # 'QHelpSearchResultWidget' + # ], + # 'QtMultimedia': [ + # 'QAbstractVideoBuffer', + # 'QAbstractVideoSurface', + # 'QAudio', + # 'QAudioDeviceInfo', + # 'QAudioFormat', + # 'QAudioInput', + # 'QAudioOutput', + # 'QVideoFrame', + # 'QVideoSurfaceFormat' + # ], + # 'QtNetwork': [ + # 'QAbstractNetworkCache', + # 'QAbstractSocket', + # 'QAuthenticator', + # 'QHostAddress', + # 'QHostInfo', + # 'QLocalServer', + # 'QLocalSocket', + # 'QNetworkAccessManager', + # 'QNetworkAddressEntry', + # 'QNetworkCacheMetaData', + # 'QNetworkConfiguration', + # 'QNetworkConfigurationManager', + # 'QNetworkCookie', + # 'QNetworkCookieJar', + # 'QNetworkDiskCache', + # 'QNetworkInterface', + # 'QNetworkProxy', + # 'QNetworkProxyFactory', + # 'QNetworkProxyQuery', + # 'QNetworkReply', + # 'QNetworkRequest', + # 'QNetworkSession', + # 'QSsl', + # 'QTcpServer', + # 'QTcpSocket', + # 'QUdpSocket' + # ], + # 'QtOpenGL': [ + # 'QGL', + # 'QGLContext', + # 'QGLFormat', + # 'QGLWidget' + # ], + # 'QtPrintSupport': [ + # 'QAbstractPrintDialog', + # 'QPageSetupDialog', + # 'QPrintDialog', + # 'QPrintEngine', + # 'QPrintPreviewDialog', + # 'QPrintPreviewWidget', + # 'QPrinter', + # 'QPrinterInfo' + # ], + # 'QtSql': [ + # 'QSql', + # 'QSqlDatabase', + # 'QSqlDriver', + # 'QSqlDriverCreatorBase', + # 'QSqlError', + # 'QSqlField', + # 'QSqlIndex', + # 'QSqlQuery', + # 'QSqlQueryModel', + # 'QSqlRecord', + # 'QSqlRelation', + # 'QSqlRelationalDelegate', + # 'QSqlRelationalTableModel', + # 'QSqlResult', + # 'QSqlTableModel' + # ], + 'QtSvg': [ + 'QGraphicsSvgItem', + 'QSvgGenerator', + 'QSvgRenderer', + 'QSvgWidget' + ], + # 'QtTest': [ + # 'QTest' + # ], + 'QtWidgets': [ + 'QAbstractButton', + 'QAbstractGraphicsShapeItem', + 'QAbstractItemDelegate', + 'QAbstractItemView', + 'QAbstractScrollArea', + 'QAbstractSlider', + 'QAbstractSpinBox', + 'QAction', + 'QActionGroup', + 'QApplication', + 'QBoxLayout', + 'QButtonGroup', + 'QCalendarWidget', + 'QCheckBox', + 'QColorDialog', + 'QColumnView', + 'QComboBox', + 'QCommandLinkButton', + 'QCommonStyle', + 'QCompleter', + 'QDataWidgetMapper', + 'QDateEdit', + 'QDateTimeEdit', + 'QDesktopWidget', + 'QDial', + 'QDialog', + 'QDialogButtonBox', + 'QDirModel', + 'QDockWidget', + 'QDoubleSpinBox', + 'QErrorMessage', + 'QFileDialog', + 'QFileIconProvider', + 'QFileSystemModel', + 'QFocusFrame', + 'QFontComboBox', + 'QFontDialog', + 'QFormLayout', + 'QFrame', + 'QGesture', + 'QGestureEvent', + 'QGestureRecognizer', + 'QGraphicsAnchor', + 'QGraphicsAnchorLayout', + 'QGraphicsBlurEffect', + 'QGraphicsColorizeEffect', + 'QGraphicsDropShadowEffect', + 'QGraphicsEffect', + 'QGraphicsEllipseItem', + 'QGraphicsGridLayout', + 'QGraphicsItem', + 'QGraphicsItemGroup', + 'QGraphicsLayout', + 'QGraphicsLayoutItem', + 'QGraphicsLineItem', + 'QGraphicsLinearLayout', + 'QGraphicsObject', + 'QGraphicsOpacityEffect', + 'QGraphicsPathItem', + 'QGraphicsPixmapItem', + 'QGraphicsPolygonItem', + 'QGraphicsProxyWidget', + 'QGraphicsRectItem', + 'QGraphicsRotation', + 'QGraphicsScale', + 'QGraphicsScene', + 'QGraphicsSceneContextMenuEvent', + 'QGraphicsSceneDragDropEvent', + 'QGraphicsSceneEvent', + 'QGraphicsSceneHelpEvent', + 'QGraphicsSceneHoverEvent', + 'QGraphicsSceneMouseEvent', + 'QGraphicsSceneMoveEvent', + 'QGraphicsSceneResizeEvent', + 'QGraphicsSceneWheelEvent', + 'QGraphicsSimpleTextItem', + 'QGraphicsTextItem', + 'QGraphicsTransform', + 'QGraphicsView', + 'QGraphicsWidget', + 'QGridLayout', + 'QGroupBox', + 'QHBoxLayout', + 'QHeaderView', + 'QInputDialog', + 'QItemDelegate', + 'QItemEditorCreatorBase', + 'QItemEditorFactory', + 'QKeyEventTransition', + 'QLCDNumber', + 'QLabel', + 'QLayout', + 'QLayoutItem', + 'QLineEdit', + 'QListView', + 'QListWidget', + 'QListWidgetItem', + 'QMainWindow', + 'QMdiArea', + 'QMdiSubWindow', + 'QMenu', + 'QMenuBar', + 'QMessageBox', + 'QMouseEventTransition', + 'QPanGesture', + 'QPinchGesture', + 'QPlainTextDocumentLayout', + 'QPlainTextEdit', + 'QProgressBar', + 'QProgressDialog', + 'QPushButton', + 'QRadioButton', + 'QRubberBand', + 'QScrollArea', + 'QScrollBar', + 'QShortcut', + 'QSizeGrip', + 'QSizePolicy', + 'QSlider', + 'QSpacerItem', + 'QSpinBox', + 'QSplashScreen', + 'QSplitter', + 'QSplitterHandle', + 'QStackedLayout', + 'QStackedWidget', + 'QStatusBar', + 'QStyle', + 'QStyleFactory', + 'QStyleHintReturn', + 'QStyleHintReturnMask', + 'QStyleHintReturnVariant', + 'QStyleOption', + 'QStyleOptionButton', + 'QStyleOptionComboBox', + 'QStyleOptionComplex', + 'QStyleOptionDockWidget', + 'QStyleOptionFocusRect', + 'QStyleOptionFrame', + 'QStyleOptionGraphicsItem', + 'QStyleOptionGroupBox', + 'QStyleOptionHeader', + 'QStyleOptionMenuItem', + 'QStyleOptionProgressBar', + 'QStyleOptionRubberBand', + 'QStyleOptionSizeGrip', + 'QStyleOptionSlider', + 'QStyleOptionSpinBox', + 'QStyleOptionTab', + 'QStyleOptionTabBarBase', + 'QStyleOptionTabWidgetFrame', + 'QStyleOptionTitleBar', + 'QStyleOptionToolBar', + 'QStyleOptionToolBox', + 'QStyleOptionToolButton', + 'QStyleOptionViewItem', + 'QStylePainter', + 'QStyledItemDelegate', + 'QSwipeGesture', + 'QSystemTrayIcon', + 'QTabBar', + 'QTabWidget', + 'QTableView', + 'QTableWidget', + 'QTableWidgetItem', + 'QTableWidgetSelectionRange', + 'QTapAndHoldGesture', + 'QTapGesture', + 'QTextBrowser', + 'QTextEdit', + 'QTimeEdit', + 'QToolBar', + 'QToolBox', + 'QToolButton', + 'QToolTip', + 'QTreeView', + 'QTreeWidget', + 'QTreeWidgetItem', + 'QTreeWidgetItemIterator', + 'QUndoCommand', + 'QUndoGroup', + 'QUndoStack', + 'QUndoView', + 'QVBoxLayout', + 'QWhatsThis', + 'QWidget', + 'QWidgetAction', + 'QWidgetItem', + 'QWizard', + 'QWizardPage' + ], + # 'QtX11Extras': [ + # 'QX11Info' + # ], + # 'QtXml': [ + # 'QDomAttr', + # 'QDomCDATASection', + # 'QDomCharacterData', + # 'QDomComment', + # 'QDomDocument', + # 'QDomDocumentFragment', + # 'QDomDocumentType', + # 'QDomElement', + # 'QDomEntity', + # 'QDomEntityReference', + # 'QDomImplementation', + # 'QDomNamedNodeMap', + # 'QDomNode', + # 'QDomNodeList', + # 'QDomNotation', + # 'QDomProcessingInstruction', + # 'QDomText', + # 'QXmlAttributes', + # 'QXmlContentHandler', + # 'QXmlDTDHandler', + # 'QXmlDeclHandler', + # 'QXmlDefaultHandler', + # 'QXmlEntityResolver', + # 'QXmlErrorHandler', + # 'QXmlInputSource', + # 'QXmlLexicalHandler', + # 'QXmlLocator', + # 'QXmlNamespaceSupport', + # 'QXmlParseException', + # 'QXmlReader', + # 'QXmlSimpleReader' + # ], + # 'QtXmlPatterns': [ + # 'QAbstractMessageHandler', + # 'QAbstractUriResolver', + # 'QAbstractXmlNodeModel', + # 'QAbstractXmlReceiver', + # 'QSourceLocation', + # 'QXmlFormatter', + # 'QXmlItem', + # 'QXmlName', + # 'QXmlNamePool', + # 'QXmlNodeModelIndex', + # 'QXmlQuery', + # 'QXmlResultItems', + # 'QXmlSchema', + # 'QXmlSchemaValidator', + # 'QXmlSerializer' + # ] + 'QtQml': [ + 'qmlRegisterType', + 'qmlRegisterUncreatableType', + 'QQmlApplicationEngine', + ], + 'QtQuick': [ + 'QQuickPaintedItem', + 'QQuickView', + ], +} + +#################################################################################################### + +"""Misplaced members + +These members from the original submodule are misplaced relative PySide2 + +""" + +_misplaced_members = { + 'PySide2': { + 'QtCore.QStringListModel': 'QtCore.QStringListModel', + 'QtGui.QStringListModel': 'QtCore.QStringListModel', + 'QtCore.Property': 'QtCore.Property', + 'QtCore.Signal': 'QtCore.Signal', + 'QtCore.Slot': 'QtCore.Slot', + 'QtCore.QAbstractProxyModel': 'QtCore.QAbstractProxyModel', + 'QtCore.QSortFilterProxyModel': 'QtCore.QSortFilterProxyModel', + 'QtCore.QItemSelection': 'QtCore.QItemSelection', + 'QtCore.QItemSelectionModel': 'QtCore.QItemSelectionModel', + 'QtCore.QItemSelectionRange': 'QtCore.QItemSelectionRange', + # 'QtUiTools.QUiLoader': ['QtCompat.loadUi', _loadUi], + # 'shiboken2.wrapInstance': ['QtCompat.wrapInstance', _wrapinstance], + # 'shiboken2.getCppPointer': ['QtCompat.getCppPointer', _getcpppointer], + 'QtWidgets.qApp': 'QtWidgets.QApplication.instance()', + # 'QtCore.QCoreApplication.translate': [ + # 'QtCompat.translate', _translate + # ], + # 'QtWidgets.QApplication.translate': [ + # 'QtCompat.translate', _translate + # ], + # 'QtCore.qInstallMessageHandler': [ + # 'QtCompat.qInstallMessageHandler', _qInstallMessageHandler + # ], + }, + 'PyQt5': { + 'QtCore.pyqtProperty': 'QtCore.Property', + 'QtCore.pyqtSignal': 'QtCore.Signal', + 'QtCore.pyqtSlot': 'QtCore.Slot', + 'QtCore.QAbstractProxyModel': 'QtCore.QAbstractProxyModel', + 'QtCore.QSortFilterProxyModel': 'QtCore.QSortFilterProxyModel', + 'QtCore.QStringListModel': 'QtCore.QStringListModel', + 'QtCore.QItemSelection': 'QtCore.QItemSelection', + 'QtCore.QItemSelectionModel': 'QtCore.QItemSelectionModel', + 'QtCore.QItemSelectionRange': 'QtCore.QItemSelectionRange', + # 'uic.loadUi': ['QtCompat.loadUi', _loadUi], + # 'sip.wrapinstance': ['QtCompat.wrapInstance', _wrapinstance], + # 'sip.unwrapinstance': ['QtCompat.getCppPointer', _getcpppointer], + 'QtWidgets.qApp': 'QtWidgets.QApplication.instance()', + # 'QtCore.QCoreApplication.translate': [ + # 'QtCompat.translate', _translate + # ], + # 'QtWidgets.QApplication.translate': [ + # 'QtCompat.translate', _translate + # ], + # 'QtCore.qInstallMessageHandler': [ + # 'QtCompat.qInstallMessageHandler', _qInstallMessageHandler + # ], + }, +} + +#################################################################################################### + +"""Compatibility Members + +This dictionary is used to build Qt.QtCompat objects that provide a consistent +interface for obsolete members, and differences in binding return values. + +{ + 'binding': { + 'classname': { + 'targetname': 'binding_namespace', + } + } +} + +""" + +_compatibility_members = { + 'PySide2': { + # 'QWidget': { + # 'grab': 'QtWidgets.QWidget.grab', + # }, + # 'QHeaderView': { + # 'sectionsClickable': 'QtWidgets.QHeaderView.sectionsClickable', + # 'setSectionsClickable': + # 'QtWidgets.QHeaderView.setSectionsClickable', + # 'sectionResizeMode': 'QtWidgets.QHeaderView.sectionResizeMode', + # 'setSectionResizeMode': + # 'QtWidgets.QHeaderView.setSectionResizeMode', + # 'sectionsMovable': 'QtWidgets.QHeaderView.sectionsMovable', + # 'setSectionsMovable': 'QtWidgets.QHeaderView.setSectionsMovable', + # }, + # 'QFileDialog': { + # 'getOpenFileName': 'QtWidgets.QFileDialog.getOpenFileName', + # 'getOpenFileNames': 'QtWidgets.QFileDialog.getOpenFileNames', + # 'getSaveFileName': 'QtWidgets.QFileDialog.getSaveFileName', + # }, + }, + 'PyQt5': { + # 'QWidget': { + # 'grab': 'QtWidgets.QWidget.grab', + # }, + # 'QHeaderView': { + # 'sectionsClickable': 'QtWidgets.QHeaderView.sectionsClickable', + # 'setSectionsClickable': + # 'QtWidgets.QHeaderView.setSectionsClickable', + # 'sectionResizeMode': 'QtWidgets.QHeaderView.sectionResizeMode', + # 'setSectionResizeMode': + # 'QtWidgets.QHeaderView.setSectionResizeMode', + # 'sectionsMovable': 'QtWidgets.QHeaderView.sectionsMovable', + # 'setSectionsMovable': 'QtWidgets.QHeaderView.setSectionsMovable', + # }, + # 'QFileDialog': { + # 'getOpenFileName': 'QtWidgets.QFileDialog.getOpenFileName', + # 'getOpenFileNames': 'QtWidgets.QFileDialog.getOpenFileNames', + # 'getSaveFileName': 'QtWidgets.QFileDialog.getSaveFileName', + # }, + }, +} diff --git a/QtShim/Wrapper.py b/QtShim/Wrapper.py new file mode 100644 index 0000000..a398f00 --- /dev/null +++ b/QtShim/Wrapper.py @@ -0,0 +1,34 @@ +#################################################################################################### + +def _qInstallMessageHandler(handler): + '''Install a message handler that works in all bindings + + Args: + handler: A function that takes 3 arguments, or None + ''' + + def messageOutputHandler(*args): + # In Qt5 bindings, message handlers are passed 3 arguments + # The first argument is a QtMsgType + # The last argument is the message to be printed + # The Middle argument (if passed) is a QMessageLogContext + if len(args) == 3: + msgType, logContext, msg = args + elif len(args) == 2: + msgType, msg = args + logContext = None + else: + raise TypeError( + 'handler expected 2 or 3 arguments, got {0}'.format(len(args))) + + if isinstance(msg, bytes): + # In python 3, some bindings pass a bytestring, which cannot be + # used elsewhere. Decoding a python 2 or 3 bytestring object will + # consistently return a unicode object. + msg = msg.decode() + + handler(msgType, logContext, msg) + + if not handler: + handler = messageOutputHandler + return Qt._QtCore.qInstallMessageHandler(handler) diff --git a/QtShim/__init__.py b/QtShim/__init__.py new file mode 100644 index 0000000..0438cc0 --- /dev/null +++ b/QtShim/__init__.py @@ -0,0 +1,389 @@ +"""Minimal Python 3 shim around PyQt5 and Pyside2 Qt bindings for QML applications. + +Forked from https://github.com/mottosso/Qt.py under MIT License. +Copyright (c) 2016 Marcus Ottosson + +Changes + +* Dropped Python2 and Qt4 support +* Focus on last Python 3 release +* Focus on last Qt API : QML + +Requirements + +* make use of lazy loading to speed up startup time ! +""" + +# Fixme: ressource file CodeReview/QtApplication/rcc/CodeReviewRessource.py + +#################################################################################################### + +# Enable support for `from Qt import *` +__all__ = [] + +#################################################################################################### + +import importlib +import logging +import os +import sys +import types + +from .QtConfig import _common_members, _misplaced_members, _compatibility_members + +#################################################################################################### + +# _module_logger = logging.getLogger(__name__) + +#################################################################################################### + +# Flags from environment variables +QT_VERBOSE = bool(os.getenv('QT_VERBOSE')) + +QT_PREFERRED_BINDING = os.getenv('QT_PREFERRED_BINDING', '') +if QT_PREFERRED_BINDING: + QT_PREFERRED_BINDING = list(x for x in QT_PREFERRED_BINDING.split(',') if x) +else: + # on dec 2018, PySide2 is still not fully operational + QT_PREFERRED_BINDING = ('PyQt5', 'PySide2') + +#################################################################################################### + +def _new_module(name): + return types.ModuleType(__name__ + '.' + name) + +#################################################################################################### + +# Reference to Qt.py +Qt = sys.modules[__name__] +Qt.QtCompat = _new_module('QtCompat') + +#################################################################################################### + +def _log(text): + if QT_VERBOSE: + # _logger print + sys.stdout.write(text + '\n') + +#################################################################################################### + +def _import_sub_module(module, name): + """import a submodule""" + _log('_import_sub_module {} {}'.format(module, name)) + module_name = module.__name__ + '.' + name # e.g. PyQt5.QtCore + module = importlib.import_module(module_name) + return module + +#################################################################################################### + +def _setup(module, extras): + """Install common submodules""" + + Qt.__binding__ = module.__name__ + + for name in list(_common_members) + extras: + try: + submodule = _import_sub_module(module, name) + except ImportError: + try: + # For extra modules like sip and shiboken that may not be + # children of the binding. + submodule = __import__(name) + except ImportError: + continue + + setattr(Qt, '_' + name, submodule) + + if name not in extras: + # Store reference to original binding + setattr(Qt, name, _new_module(name)) # Qt.QtCore = module(so module) + +#################################################################################################### + +def _reassign_misplaced_members(binding): + """Apply misplaced members from `binding` to Qt.py + + Arguments: + binding (dict): Misplaced members + + """ + + for src, dst in _misplaced_members[binding].items(): + # print() + dst_value = None + + # Fixme: to func + src_parts = src.split('.') + src_module = src_parts[0] + if len(src_parts): + src_member = src_parts[1:] + else: + src_member = None + + if isinstance(dst, (list, tuple)): + dst, dst_value = dst + # print(src, '->', dst, dst_value) + # print(src_module, src_member) + + dst_parts = dst.split('.') + dst_module = dst_parts[0] + if len(dst_parts): + dst_member = dst_parts[1] + else: + dst_member = None + # print(dst_module, dst_member) + + # Get the member we want to store in the namesapce. + if not dst_value: + try: + _part = getattr(Qt, '_' + src_module) + while src_member: + member = src_member.pop(0) + _part = getattr(_part, member) + dst_value = _part + except AttributeError: + # If the member we want to store in the namespace does not + # exist, there is no need to continue. This can happen if a + # request was made to rename a member that didn't exist, for + # example if QtWidgets isn't available on the target platform. + _log('Misplaced member has no source: {0}'.format(src)) + continue + # print(dst_value) + + try: + # Fixme: src_object ??? + src_object = getattr(Qt, dst_module) + except AttributeError: + # print('Failed to get src_object') + if dst_module not in _common_members: + # Only create the Qt parent module if its listed in + # _common_members. Without this check, if you remove QtCore + # from _common_members, the default _misplaced_members will add + # Qt.QtCore so it can add Signal, Slot, etc. + msg = "Not creating missing member module '{m}' for '{c}'" + _log(msg.format(m=dst_module, c=dst_member)) + continue + # If the dst is valid but the Qt parent module does not exist + # then go ahead and create a new module to contain the member. + setattr(Qt, dst_module, _new_module(dst_module)) + src_object = getattr(Qt, dst_module) + # Enable direct import of the new module + sys.modules[__name__ + '.' + dst_module] = src_object + + if not dst_value: + dst_value = getattr(Qt, '_' + src_module) + if src_member: + dst_value = getattr(dst_value, src_member) + + setattr( + src_object, + dst_member or dst_module, + dst_value + ) + +#################################################################################################### + +def _build_compatibility_members(binding, decorators=None): + """Apply `binding` to QtCompat + + Arguments: + binding (str): Top level binding in _compatibility_members. + decorators (dict, optional): Provides the ability to decorate the + original Qt methods when needed by a binding. This can be used + to change the returned value to a standard value. The key should + be the classname, the value is a dict where the keys are the + target method names, and the values are the decorator functions. + + """ + + decorators = decorators or dict() + + # Allow optional site-level customization of the compatibility members. + # This method does not need to be implemented in QtSiteConfig. + try: + import QtSiteConfig + except ImportError: + pass + else: + if hasattr(QtSiteConfig, 'update_compatibility_decorators'): + QtSiteConfig.update_compatibility_decorators(binding, decorators) + + _QtCompat = type('QtCompat', (object,), {}) + + for classname, bindings in _compatibility_members[binding].items(): + attrs = {} + for target, binding in bindings.items(): + namespaces = binding.split('.') + try: + src_object = getattr(Qt, '_' + namespaces[0]) + except AttributeError as e: + _log('QtCompat: AttributeError: %s' % e) + # Skip reassignment of non-existing members. + # This can happen if a request was made to + # rename a member that didn't exist, for example + # if QtWidgets isn't available on the target platform. + continue + + # Walk down any remaining namespace getting the object assuming + # that if the first namespace exists the rest will exist. + for namespace in namespaces[1:]: + src_object = getattr(src_object, namespace) + + # decorate the Qt method if a decorator was provided. + if target in decorators.get(classname, []): + # staticmethod must be called on the decorated method to + # prevent a TypeError being raised when the decorated method + # is called. + src_object = staticmethod( + decorators[classname][target](src_object)) + + attrs[target] = src_object + + # Create the QtCompat class and install it into the namespace + compat_class = type(classname, (_QtCompat,), attrs) + setattr(Qt.QtCompat, classname, compat_class) + +#################################################################################################### + +def _pyside2(): + """Initialise PySide2 + + These functions serve to test the existence of a binding + along with set it up in such a way that it aligns with + the final step; adding members from the original binding + to Qt.py + + """ + + import PySide2 as module + extras = [] + # try: + # from PySide2 import shiboken2 + # extras.append('shiboken2') + # except ImportError: + # pass + + _setup(module, extras) + Qt.__binding_version__ = module.__version__ + + # if hasattr(Qt, '_shiboken2'): + # Qt.QtCompat.wrapInstance = _wrapinstance + # Qt.QtCompat.getCppPointer = _getcpppointer + # Qt.QtCompat.delete = shiboken2.delete + + if hasattr(Qt, '_QtCore'): + Qt.__qt_version__ = Qt._QtCore.qVersion() + + # if hasattr(Qt, '_QtWidgets'): + # Qt.QtCompat.setSectionResizeMode = \ + # Qt._QtWidgets.QHeaderView.setSectionResizeMode + + _reassign_misplaced_members('PySide2') + # _build_compatibility_members('PySide2') + +#################################################################################################### + +def _pyqt5(): + """Initialise PyQt5""" + + import PyQt5 as module + extras = [] + # try: + # import sip + # extras.append(sip.__name__) + # except ImportError: + # sip = None + + _setup(module, extras) + # if hasattr(Qt, '_sip'): + # Qt.QtCompat.wrapInstance = _wrapinstance + # Qt.QtCompat.getCppPointer = _getcpppointer + # Qt.QtCompat.delete = sip.delete + + if hasattr(Qt, '_QtCore'): + Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR + Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR + + # if hasattr(Qt, '_QtWidgets'): + # Qt.QtCompat.setSectionResizeMode = \ + # Qt._QtWidgets.QHeaderView.setSectionResizeMode + + _reassign_misplaced_members('PyQt5') + # _build_compatibility_members('PyQt5') + +#################################################################################################### + +def _install(): + + # Default order (customise order and content via QT_PREFERRED_BINDING) + order = QT_PREFERRED_BINDING + + available = { + 'PySide2': _pyside2, + 'PyQt5': _pyqt5, + } + + _log("Order: {}".format(' '.join(order))) + + found_binding = False + for name in order: + _log('Trying %s' % name) + + try: + available[name]() + found_binding = True + break + + except ImportError as e: + _log('ImportError: %s' % e) + + except KeyError: + _log("ImportError: Preferred binding '%s' not found." % name) + + if not found_binding: + # If not binding were found, throw this error + raise ImportError('No Qt binding were found.') + + # Install individual members + for name, members in _common_members.items(): + try: + their_submodule = getattr(Qt, '_' + name) + except AttributeError: + continue + + our_submodule = getattr(Qt, name) + + # Enable import * + __all__.append(name) + + # Enable direct import of submodule, + # e.g. import Qt.QtCore + sys.modules[__name__ + '.' + name] = our_submodule + + for member in members: + # Accept that a submodule may miss certain members. + try: + their_member = getattr(their_submodule, member) + except AttributeError: + _log("'%s.%s' was missing." % (name, member)) + continue + setattr(our_submodule, member, their_member) + + # Enable direct import of QtCompat + sys.modules['Qt.QtCompat'] = Qt.QtCompat + +#################################################################################################### + +_install() + +#################################################################################################### + +# Fixme: Python 3.7 +# def __getattr__(name): +# print('__getattr__', name) + +#################################################################################################### + +# Setup Binding Enum states +Qt.IsPySide2 = Qt.__binding__ == 'PySide2' +Qt.IsPyQt5 = not Qt.IsPySide2 diff --git a/QtShim/not-implemented.py b/QtShim/not-implemented.py new file mode 100644 index 0000000..be022e1 --- /dev/null +++ b/QtShim/not-implemented.py @@ -0,0 +1,116 @@ +#################################################################################################### + +def _getcpppointer(object): + if hasattr(Qt, '_shiboken2'): + return getattr(Qt, '_shiboken2').getCppPointer(object)[0] + elif hasattr(Qt, '_sip'): + return getattr(Qt, '_sip').unwrapinstance(object) + raise AttributeError("'module' has no attribute 'getCppPointer'") + +#################################################################################################### + +def _wrapinstance(ptr, base=None): + '''Enable implicit cast of pointer to most suitable class + + This behaviour is available in sip per default. + + Based on http://nathanhorne.com/pyqtpyside-wrap-instance + + Usage: + This mechanism kicks in under these circumstances. + 1. Qt.py is using PySide 1 or 2. + 2. A `base` argument is not provided. + + See :func:`QtCompat.wrapInstance()` + + Arguments: + ptr (int): Pointer to QObject in memory + base (QObject, optional): Base class to wrap with. Defaults to QObject, + which should handle anything. + + ''' + + assert isinstance(ptr, int), "Argument 'ptr' must be of type " + assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( + "Argument 'base' must be of type ") + + if Qt.IsPyQt4 or Qt.IsPyQt5: + func = getattr(Qt, '_sip').wrapinstance + elif Qt.IsPySide2: + func = getattr(Qt, '_shiboken2').wrapInstance + elif Qt.IsPySide: + func = getattr(Qt, '_shiboken').wrapInstance + else: + raise AttributeError("'module' has no attribute 'wrapInstance'") + + if base is None: + q_object = func(int(ptr), Qt.QtCore.QObject) + meta_object = q_object.metaObject() + class_name = meta_object.className() + super_class_name = meta_object.superClass().className() + + if hasattr(Qt.QtWidgets, class_name): + base = getattr(Qt.QtWidgets, class_name) + + elif hasattr(Qt.QtWidgets, super_class_name): + base = getattr(Qt.QtWidgets, super_class_name) + + else: + base = Qt.QtCore.QObject + + return func(int(ptr), base) + +#################################################################################################### + +def _translate(context, sourceText, *args): + # In Qt4 bindings, translate can be passed 2 or 3 arguments + # In Qt5 bindings, translate can be passed 2 arguments + # The first argument is disambiguation[str] + # The last argument is n[int] + # The middle argument can be encoding[QtCore.QCoreApplication.Encoding] + if len(args) == 3: + disambiguation, encoding, n = args + elif len(args) == 2: + disambiguation, n = args + encoding = None + else: + raise TypeError( + 'Expected 4 or 5 arguments, got {0}.'.format(len(args) + 2)) + + if hasattr(Qt.QtCore, 'QCoreApplication'): + app = getattr(Qt.QtCore, 'QCoreApplication') + else: + raise NotImplementedError( + 'Missing QCoreApplication implementation for {binding}'.format( + binding=Qt.__binding__, + ) + ) + if Qt.__binding__ in ('PySide2', 'PyQt5'): + sanitized_args = [context, sourceText, disambiguation, n] + else: + sanitized_args = [ + context, + sourceText, + disambiguation, + encoding or app.CodecForTr, + n + ] + return app.translate(*sanitized_args) + +#################################################################################################### +#################################################################################################### + +def _none(): + '''Internal option (used in installer)''' + + Mock = type('Mock', (), {'__getattr__': lambda Qt, attr: None}) + + Qt.__binding__ = 'None' + Qt.__qt_version__ = '0.0.0' + Qt.__binding_version__ = '0.0.0' + Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None + Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None + + for submodule in _common_members.keys(): + setattr(Qt, submodule, Mock()) + setattr(Qt, '_' + submodule, Mock()) From dad39afea5fb0ad8d1199eb7dc37e000fb117080 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 13:50:07 +0200 Subject: [PATCH 19/69] Tools -> Common --- CodeReview/Application/ApplicationBase.py | 4 ++-- .../AttributeDictionaryInterface.py | 0 CodeReview/{Tools => Common}/BackTrace.py | 0 CodeReview/{Tools => Common}/EnumFactory.py | 0 CodeReview/{Tools => Common}/IteratorTools.py | 0 .../{ => Common}/Logging/ExceptionHook.py | 4 ++-- CodeReview/{ => Common}/Logging/Logging.py | 4 ++-- CodeReview/{ => Common}/Logging/__init__.py | 0 CodeReview/{ => Common}/Math/Functions.py | 0 CodeReview/{ => Common}/Math/Interval.py | 2 +- CodeReview/{ => Common}/Math/__init__.py | 0 CodeReview/{Tools => Common}/Path.py | 0 CodeReview/{Tools => Common}/Platform.py | 4 ++-- CodeReview/{Tools => Common}/ProgramOptions.py | 2 +- CodeReview/{Tools => Common}/RevisionVersion.py | 0 CodeReview/{Tools => Common}/Singleton.py | 0 CodeReview/{Tools => Common}/Slice.py | 0 CodeReview/{Tools => Common}/StringTools.py | 0 CodeReview/Common/__init__.py | 0 CodeReview/Config/ConfigInstall.py | 2 +- CodeReview/Diff/RawTextDocument.py | 4 ++-- CodeReview/Diff/RawTextDocumentDiff.py | 4 ++-- CodeReview/Diff/SyntaxHighlighter.py | 2 +- CodeReview/Diff/TextDocumentDiffModel.py | 2 +- .../GUI/DiffViewer/DiffViewerMainWindow.py | 2 +- CodeReview/GUI/DiffViewer/DiffWidget.py | 6 +++--- CodeReview/GUI/Forms/CriticalErrorForm.py | 4 ++-- CodeReview/GUI/LogBrowser/CommitTableModel.py | 2 +- CodeReview/GUI/LogBrowser/LogTableModel.py | 2 +- CodeReview/GUI/Widgets/IconLoader.py | 2 +- CodeReview/Tools/__init__.py | 17 ----------------- CodeReview/Version.py | 2 +- CodeReview/Version.py.in | 2 +- bin/diff-viewer | 4 ++-- bin/pyqgit | 4 ++-- 35 files changed, 32 insertions(+), 49 deletions(-) rename CodeReview/{Tools => Common}/AttributeDictionaryInterface.py (100%) rename CodeReview/{Tools => Common}/BackTrace.py (100%) rename CodeReview/{Tools => Common}/EnumFactory.py (100%) rename CodeReview/{Tools => Common}/IteratorTools.py (100%) rename CodeReview/{ => Common}/Logging/ExceptionHook.py (97%) rename CodeReview/{ => Common}/Logging/Logging.py (94%) rename CodeReview/{ => Common}/Logging/__init__.py (100%) rename CodeReview/{ => Common}/Math/Functions.py (100%) rename CodeReview/{ => Common}/Math/Interval.py (99%) rename CodeReview/{ => Common}/Math/__init__.py (100%) rename CodeReview/{Tools => Common}/Path.py (100%) rename CodeReview/{Tools => Common}/Platform.py (98%) rename CodeReview/{Tools => Common}/ProgramOptions.py (97%) rename CodeReview/{Tools => Common}/RevisionVersion.py (100%) rename CodeReview/{Tools => Common}/Singleton.py (100%) rename CodeReview/{Tools => Common}/Slice.py (100%) rename CodeReview/{Tools => Common}/StringTools.py (100%) create mode 100644 CodeReview/Common/__init__.py delete mode 100644 CodeReview/Tools/__init__.py diff --git a/CodeReview/Application/ApplicationBase.py b/CodeReview/Application/ApplicationBase.py index 58cc0c0..91ce959 100644 --- a/CodeReview/Application/ApplicationBase.py +++ b/CodeReview/Application/ApplicationBase.py @@ -26,8 +26,8 @@ #################################################################################################### -from CodeReview.Tools.Path import to_absolute_path -from CodeReview.Tools.Platform import Platform +from CodeReview.Common.Path import to_absolute_path +from CodeReview.Common.Platform import Platform #################################################################################################### diff --git a/CodeReview/Tools/AttributeDictionaryInterface.py b/CodeReview/Common/AttributeDictionaryInterface.py similarity index 100% rename from CodeReview/Tools/AttributeDictionaryInterface.py rename to CodeReview/Common/AttributeDictionaryInterface.py diff --git a/CodeReview/Tools/BackTrace.py b/CodeReview/Common/BackTrace.py similarity index 100% rename from CodeReview/Tools/BackTrace.py rename to CodeReview/Common/BackTrace.py diff --git a/CodeReview/Tools/EnumFactory.py b/CodeReview/Common/EnumFactory.py similarity index 100% rename from CodeReview/Tools/EnumFactory.py rename to CodeReview/Common/EnumFactory.py diff --git a/CodeReview/Tools/IteratorTools.py b/CodeReview/Common/IteratorTools.py similarity index 100% rename from CodeReview/Tools/IteratorTools.py rename to CodeReview/Common/IteratorTools.py diff --git a/CodeReview/Logging/ExceptionHook.py b/CodeReview/Common/Logging/ExceptionHook.py similarity index 97% rename from CodeReview/Logging/ExceptionHook.py rename to CodeReview/Common/Logging/ExceptionHook.py index 6a58b4c..b005ede 100644 --- a/CodeReview/Logging/ExceptionHook.py +++ b/CodeReview/Common/Logging/ExceptionHook.py @@ -25,8 +25,8 @@ #################################################################################################### -from CodeReview.Tools.Platform import Platform -from CodeReview.Tools.Singleton import singleton +from CodeReview.Common.Platform import Platform +from CodeReview.Common.Singleton import singleton #################################################################################################### diff --git a/CodeReview/Logging/Logging.py b/CodeReview/Common/Logging/Logging.py similarity index 94% rename from CodeReview/Logging/Logging.py rename to CodeReview/Common/Logging/Logging.py index 9d26302..ff9ae0d 100644 --- a/CodeReview/Logging/Logging.py +++ b/CodeReview/Common/Logging/Logging.py @@ -24,8 +24,8 @@ #################################################################################################### -from CodeReview.Logging.ExceptionHook import DispatcherExceptionHook, StderrExceptionHook -from CodeReview.Tools.Singleton import singleton +from .ExceptionHook import DispatcherExceptionHook, StderrExceptionHook +from CodeReview.Common.Singleton import singleton import CodeReview.Config.ConfigInstall as ConfigInstall #################################################################################################### diff --git a/CodeReview/Logging/__init__.py b/CodeReview/Common/Logging/__init__.py similarity index 100% rename from CodeReview/Logging/__init__.py rename to CodeReview/Common/Logging/__init__.py diff --git a/CodeReview/Math/Functions.py b/CodeReview/Common/Math/Functions.py similarity index 100% rename from CodeReview/Math/Functions.py rename to CodeReview/Common/Math/Functions.py diff --git a/CodeReview/Math/Interval.py b/CodeReview/Common/Math/Interval.py similarity index 99% rename from CodeReview/Math/Interval.py rename to CodeReview/Common/Math/Interval.py index c529cd0..31d9897 100644 --- a/CodeReview/Math/Interval.py +++ b/CodeReview/Common/Math/Interval.py @@ -42,7 +42,7 @@ #################################################################################################### -from CodeReview.Math.Functions import middle +from CodeReview.Common.Math.Functions import middle #################################################################################################### diff --git a/CodeReview/Math/__init__.py b/CodeReview/Common/Math/__init__.py similarity index 100% rename from CodeReview/Math/__init__.py rename to CodeReview/Common/Math/__init__.py diff --git a/CodeReview/Tools/Path.py b/CodeReview/Common/Path.py similarity index 100% rename from CodeReview/Tools/Path.py rename to CodeReview/Common/Path.py diff --git a/CodeReview/Tools/Platform.py b/CodeReview/Common/Platform.py similarity index 98% rename from CodeReview/Tools/Platform.py rename to CodeReview/Common/Platform.py index 709cd35..256cbe2 100644 --- a/CodeReview/Tools/Platform.py +++ b/CodeReview/Common/Platform.py @@ -26,8 +26,8 @@ #################################################################################################### -from CodeReview.Tools.EnumFactory import EnumFactory -from CodeReview.Math.Functions import rint +from CodeReview.Common.EnumFactory import EnumFactory +from CodeReview.Common.Math.Functions import rint #################################################################################################### diff --git a/CodeReview/Tools/ProgramOptions.py b/CodeReview/Common/ProgramOptions.py similarity index 97% rename from CodeReview/Tools/ProgramOptions.py rename to CodeReview/Common/ProgramOptions.py index 0860aa0..b4e5dbf 100644 --- a/CodeReview/Tools/ProgramOptions.py +++ b/CodeReview/Common/ProgramOptions.py @@ -22,7 +22,7 @@ #################################################################################################### -from CodeReview.Tools.Path import to_absolute_path +from CodeReview.Common.Path import to_absolute_path #################################################################################################### diff --git a/CodeReview/Tools/RevisionVersion.py b/CodeReview/Common/RevisionVersion.py similarity index 100% rename from CodeReview/Tools/RevisionVersion.py rename to CodeReview/Common/RevisionVersion.py diff --git a/CodeReview/Tools/Singleton.py b/CodeReview/Common/Singleton.py similarity index 100% rename from CodeReview/Tools/Singleton.py rename to CodeReview/Common/Singleton.py diff --git a/CodeReview/Tools/Slice.py b/CodeReview/Common/Slice.py similarity index 100% rename from CodeReview/Tools/Slice.py rename to CodeReview/Common/Slice.py diff --git a/CodeReview/Tools/StringTools.py b/CodeReview/Common/StringTools.py similarity index 100% rename from CodeReview/Tools/StringTools.py rename to CodeReview/Common/StringTools.py diff --git a/CodeReview/Common/__init__.py b/CodeReview/Common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CodeReview/Config/ConfigInstall.py b/CodeReview/Config/ConfigInstall.py index 260b5f6..cd39a69 100644 --- a/CodeReview/Config/ConfigInstall.py +++ b/CodeReview/Config/ConfigInstall.py @@ -5,7 +5,7 @@ #################################################################################################### -import CodeReview.Tools.Path as PathTools # due to Path class +import CodeReview.Common.Path as PathTools # due to Path class #################################################################################################### diff --git a/CodeReview/Diff/RawTextDocument.py b/CodeReview/Diff/RawTextDocument.py index 2b05ab1..8a4a1bb 100644 --- a/CodeReview/Diff/RawTextDocument.py +++ b/CodeReview/Diff/RawTextDocument.py @@ -56,8 +56,8 @@ #################################################################################################### -from CodeReview.Tools.Slice import FlatSlice, LineSlice -from CodeReview.Tools.IteratorTools import pairwise +from CodeReview.Common.Slice import FlatSlice, LineSlice +from CodeReview.Common.IteratorTools import pairwise #################################################################################################### diff --git a/CodeReview/Diff/RawTextDocumentDiff.py b/CodeReview/Diff/RawTextDocumentDiff.py index b2120f8..88439b7 100644 --- a/CodeReview/Diff/RawTextDocumentDiff.py +++ b/CodeReview/Diff/RawTextDocumentDiff.py @@ -63,8 +63,8 @@ #################################################################################################### -from CodeReview.Tools.EnumFactory import EnumFactory -from CodeReview.Tools.Slice import FlatSlice, LineSlice +from CodeReview.Common.EnumFactory import EnumFactory +from CodeReview.Common.Slice import FlatSlice, LineSlice #################################################################################################### diff --git a/CodeReview/Diff/SyntaxHighlighter.py b/CodeReview/Diff/SyntaxHighlighter.py index 062cdfd..ba8a6bd 100644 --- a/CodeReview/Diff/SyntaxHighlighter.py +++ b/CodeReview/Diff/SyntaxHighlighter.py @@ -33,7 +33,7 @@ #################################################################################################### -from CodeReview.Tools.Slice import FlatSlice +from CodeReview.Common.Slice import FlatSlice from CodeReview.Diff.TextDocumentModel import TextDocumentModel, TextBlock, TextFragment #################################################################################################### diff --git a/CodeReview/Diff/TextDocumentDiffModel.py b/CodeReview/Diff/TextDocumentDiffModel.py index 79ca2d5..4e4d6d0 100644 --- a/CodeReview/Diff/TextDocumentDiffModel.py +++ b/CodeReview/Diff/TextDocumentDiffModel.py @@ -30,7 +30,7 @@ from .RawTextDocumentDiff import chunk_type from .TextDocumentModel import TextDocumentModel, TextBlock, TextFragment -from CodeReview.Tools.Slice import LineSlice +from CodeReview.Common.Slice import LineSlice #################################################################################################### diff --git a/CodeReview/GUI/DiffViewer/DiffViewerMainWindow.py b/CodeReview/GUI/DiffViewer/DiffViewerMainWindow.py index 77cfbb1..0e91b49 100644 --- a/CodeReview/GUI/DiffViewer/DiffViewerMainWindow.py +++ b/CodeReview/GUI/DiffViewer/DiffViewerMainWindow.py @@ -409,7 +409,7 @@ def diff_text_documents(self, show=False): # cursor.begin_block(side, text_block.frame_type) # File "/home/gv/fabrice/unison-osiris/git-python/CodeReview/DiffWidget.py", line 99, in begin_block # if ((side == LEFT and frame_type == chunk_type.insert) or - # File "/home/gv/fabrice/unison-osiris/git-python/CodeReview/Tools/EnumFactory.py", line 107, in __eq__ + # File "/home/gv/fabrice/unison-osiris/git-python/CodeReview/Common/EnumFactory.py", line 107, in __eq__ # return self._value == int(other) # TypeError: int() argument must be a string or a number, not 'NoneType' for raw_text_document, lexer in zip(raw_text_documents, self._lexers): diff --git a/CodeReview/GUI/DiffViewer/DiffWidget.py b/CodeReview/GUI/DiffViewer/DiffWidget.py index 3ecebc6..1af21e1 100644 --- a/CodeReview/GUI/DiffViewer/DiffWidget.py +++ b/CodeReview/GUI/DiffViewer/DiffWidget.py @@ -27,9 +27,9 @@ #################################################################################################### from CodeReview.Diff.RawTextDocumentDiff import chunk_type -from CodeReview.Math.Functions import number_of_digits -from CodeReview.Tools.IteratorTools import pairwise, iter_with_last_flag -from CodeReview.Tools.StringTools import remove_trailing_newline +from CodeReview.Common.Math.Functions import number_of_digits +from CodeReview.Common.IteratorTools import pairwise, iter_with_last_flag +from CodeReview.Common.StringTools import remove_trailing_newline import CodeReview.GUI.DiffViewer.DiffWidgetConfig as DiffWidgetConfig from .SyntaxHighlighterStyle import SyntaxHighlighterStyle diff --git a/CodeReview/GUI/Forms/CriticalErrorForm.py b/CodeReview/GUI/Forms/CriticalErrorForm.py index ce74c7d..68054b6 100644 --- a/CodeReview/GUI/Forms/CriticalErrorForm.py +++ b/CodeReview/GUI/Forms/CriticalErrorForm.py @@ -26,8 +26,8 @@ #################################################################################################### -from CodeReview.Logging.ExceptionHook import format_exception -import CodeReview.Tools.BackTrace as BackTrace +from CodeReview.Common.Logging.ExceptionHook import format_exception +import CodeReview.Common.BackTrace as BackTrace #################################################################################################### diff --git a/CodeReview/GUI/LogBrowser/CommitTableModel.py b/CodeReview/GUI/LogBrowser/CommitTableModel.py index 40028a3..40d0430 100644 --- a/CodeReview/GUI/LogBrowser/CommitTableModel.py +++ b/CodeReview/GUI/LogBrowser/CommitTableModel.py @@ -27,7 +27,7 @@ #################################################################################################### -from CodeReview.Tools.EnumFactory import EnumFactory +from CodeReview.Common.EnumFactory import EnumFactory #################################################################################################### diff --git a/CodeReview/GUI/LogBrowser/LogTableModel.py b/CodeReview/GUI/LogBrowser/LogTableModel.py index 19563f5..0447b17 100644 --- a/CodeReview/GUI/LogBrowser/LogTableModel.py +++ b/CodeReview/GUI/LogBrowser/LogTableModel.py @@ -30,7 +30,7 @@ #################################################################################################### -from CodeReview.Tools.EnumFactory import EnumFactory +from CodeReview.Common.EnumFactory import EnumFactory #################################################################################################### diff --git a/CodeReview/GUI/Widgets/IconLoader.py b/CodeReview/GUI/Widgets/IconLoader.py index bf0ffb3..fafc85a 100644 --- a/CodeReview/GUI/Widgets/IconLoader.py +++ b/CodeReview/GUI/Widgets/IconLoader.py @@ -25,7 +25,7 @@ #################################################################################################### -from CodeReview.Tools.Singleton import SingletonMetaClass +from CodeReview.Common.Singleton import SingletonMetaClass import CodeReview.Config.ConfigInstall as ConfigInstall #################################################################################################### diff --git a/CodeReview/Tools/__init__.py b/CodeReview/Tools/__init__.py deleted file mode 100644 index f523b65..0000000 --- a/CodeReview/Tools/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -#################################################################################################### -# -# CodeReview - A Code Review GUI -# Copyright (C) 2015 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program. If -# not, see . -# -#################################################################################################### diff --git a/CodeReview/Version.py b/CodeReview/Version.py index dfb1c7d..5b08740 100644 --- a/CodeReview/Version.py +++ b/CodeReview/Version.py @@ -18,7 +18,7 @@ #################################################################################################### -from CodeReview.Tools.RevisionVersion import RevisionVersion +from CodeReview.Common.RevisionVersion import RevisionVersion #################################################################################################### diff --git a/CodeReview/Version.py.in b/CodeReview/Version.py.in index 75a4b82..2354e2a 100644 --- a/CodeReview/Version.py.in +++ b/CodeReview/Version.py.in @@ -20,7 +20,7 @@ #################################################################################################### -from CodeReview.Tools.RevisionVersion import RevisionVersion +from CodeReview.Common.RevisionVersion import RevisionVersion #################################################################################################### diff --git a/bin/diff-viewer b/bin/diff-viewer index c44d25d..4441035 100755 --- a/bin/diff-viewer +++ b/bin/diff-viewer @@ -25,7 +25,7 @@ # Logging # -import CodeReview.Logging.Logging as Logging +import CodeReview.Common.Logging.Logging as Logging logger = Logging.setup_logging('pyqgit') @@ -36,7 +36,7 @@ import argparse #################################################################################################### from CodeReview.GUI.DiffViewer.DiffViewerApplication import DiffViewerApplication -from CodeReview.Tools.ProgramOptions import PathAction +from CodeReview.Common.ProgramOptions import PathAction #################################################################################################### # diff --git a/bin/pyqgit b/bin/pyqgit index 6f5ead1..7160f96 100755 --- a/bin/pyqgit +++ b/bin/pyqgit @@ -25,7 +25,7 @@ # Logging # -import CodeReview.Logging.Logging as Logging +import CodeReview.Common.Logging.Logging as Logging logger = Logging.setup_logging('pyqgit') @@ -36,7 +36,7 @@ import argparse #################################################################################################### from CodeReview.GUI.LogBrowser.LogBrowserApplication import LogBrowserApplication -from CodeReview.Tools.ProgramOptions import PathAction +from CodeReview.Common.ProgramOptions import PathAction #################################################################################################### # From b7333db60a3540967045a4b81ddb242ef635c2fe Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 13:50:19 +0200 Subject: [PATCH 20/69] add missing Review --- CodeReview/Review/__init__.py | 89 +++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 CodeReview/Review/__init__.py diff --git a/CodeReview/Review/__init__.py b/CodeReview/Review/__init__.py new file mode 100644 index 0000000..011a5cd --- /dev/null +++ b/CodeReview/Review/__init__.py @@ -0,0 +1,89 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2020 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU +# General Public License as published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If +# not, see . +# +#################################################################################################### + +__all__ = ['Review'] + +#################################################################################################### + +from pathlib import Path + +import json + +#################################################################################################### + +class ReviewNote: + + ############################################## + + def __init__(self, sha, text=''): + + self._sha = str(sha) + self._text = str(text) + + ############################################## + + @property + def sha(self): + return self._sha + + @property + def text(self): + return self._text + + @text.setter + def text(self, value): + self._text = str(value) + + ############################################## + + def to_json(self): + return {key:getattr(self, key) for key in ('text',)} + +#################################################################################################### + +class Review: + + ############################################## + + def __init__(self, path): + + self._path = Path(path) + + self._notes = {} + if self._path.exists(): + with open(self._path) as fh: + data = json.load(fh) + for sha, review in data.items(): + self.add(ReviewNote(sha, **review)) + + ############################################## + + def save(self): + with open(self._path, 'w') as fh: + data = {note.sha:note.to_json() for note in self._notes.values()} + json.dump(data, fh, indent=4, sort_keys=True, ensure_ascii=False) + + ############################################## + + def __getitem__(self, sha): + return self._notes.get(sha, None) + + ############################################## + + def add(self, review_note): + self._notes[review_note.sha] = review_note From 7a21718672c295833c9360d3e6cdc82aaef4f9e2 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 15:39:00 +0200 Subject: [PATCH 21/69] bootstrap a qml application (imported from book-browser project) --- CodeReview/Common/ArgparseAction.py | 53 +++ CodeReview/Common/Platform2.py | 285 ++++++++++++ .../QmlApplication/ApplicationMetadata.py | 69 +++ .../QmlApplication/ApplicationSettings.py | 173 ++++++++ CodeReview/QmlApplication/DefaultSettings.py | 36 ++ .../QmlApplication/KeySequenceEditor.py | 316 ++++++++++++++ CodeReview/QmlApplication/QmlApplication.py | 405 ++++++++++++++++++ CodeReview/QmlApplication/__init__.py | 0 .../QmlApplication/qml/Constants/Style.qml | 74 ++++ .../QmlApplication/qml/Constants/qmldir | 2 + CodeReview/QmlApplication/qml/Controls/qmldir | 1 + .../qml/UserInterface/Actions.qml | 28 ++ .../qml/UserInterface/FooterToolBar.qml | 43 ++ .../qml/UserInterface/HeaderToolBar.qml | 44 ++ .../qml/UserInterface/MenuBar.qml | 84 ++++ .../qml/UserInterface/OptionsDialog.qml | 74 ++++ .../QmlApplication/qml/UserInterface/qmldir | 6 + .../qml/Widgets/AboutDialog.qml | 49 +++ .../qml/Widgets/CentredDialog.qml | 29 ++ .../qml/Widgets/ErrorMessageDialog.qml | 52 +++ .../qml/Widgets/MarkdownViewer.qml | 129 ++++++ .../qml/Widgets/ShortcutRow.qml | 105 +++++ CodeReview/QmlApplication/qml/Widgets/qmldir | 6 + CodeReview/QmlApplication/qml/main.qml | 173 ++++++++ CodeReview/QmlApplication/rcc/Makefile | 1 + CodeReview/QmlApplication/rcc/__init__.py | 0 CodeReview/QmlApplication/rcc/code-review.qrc | 17 + .../QmlApplication/rcc/qtquickcontrols2.conf | 2 + CodeReview/__init__.py | 1 + bin/pyqgit-qml | 37 ++ 30 files changed, 2294 insertions(+) create mode 100644 CodeReview/Common/ArgparseAction.py create mode 100644 CodeReview/Common/Platform2.py create mode 100644 CodeReview/QmlApplication/ApplicationMetadata.py create mode 100644 CodeReview/QmlApplication/ApplicationSettings.py create mode 100644 CodeReview/QmlApplication/DefaultSettings.py create mode 100644 CodeReview/QmlApplication/KeySequenceEditor.py create mode 100644 CodeReview/QmlApplication/QmlApplication.py create mode 100644 CodeReview/QmlApplication/__init__.py create mode 100644 CodeReview/QmlApplication/qml/Constants/Style.qml create mode 100644 CodeReview/QmlApplication/qml/Constants/qmldir create mode 100644 CodeReview/QmlApplication/qml/Controls/qmldir create mode 100644 CodeReview/QmlApplication/qml/UserInterface/Actions.qml create mode 100644 CodeReview/QmlApplication/qml/UserInterface/FooterToolBar.qml create mode 100644 CodeReview/QmlApplication/qml/UserInterface/HeaderToolBar.qml create mode 100644 CodeReview/QmlApplication/qml/UserInterface/MenuBar.qml create mode 100644 CodeReview/QmlApplication/qml/UserInterface/OptionsDialog.qml create mode 100644 CodeReview/QmlApplication/qml/UserInterface/qmldir create mode 100644 CodeReview/QmlApplication/qml/Widgets/AboutDialog.qml create mode 100644 CodeReview/QmlApplication/qml/Widgets/CentredDialog.qml create mode 100644 CodeReview/QmlApplication/qml/Widgets/ErrorMessageDialog.qml create mode 100644 CodeReview/QmlApplication/qml/Widgets/MarkdownViewer.qml create mode 100644 CodeReview/QmlApplication/qml/Widgets/ShortcutRow.qml create mode 100644 CodeReview/QmlApplication/qml/Widgets/qmldir create mode 100644 CodeReview/QmlApplication/qml/main.qml create mode 120000 CodeReview/QmlApplication/rcc/Makefile create mode 100644 CodeReview/QmlApplication/rcc/__init__.py create mode 100644 CodeReview/QmlApplication/rcc/code-review.qrc create mode 100644 CodeReview/QmlApplication/rcc/qtquickcontrols2.conf create mode 100755 bin/pyqgit-qml diff --git a/CodeReview/Common/ArgparseAction.py b/CodeReview/Common/ArgparseAction.py new file mode 100644 index 0000000..24e5097 --- /dev/null +++ b/CodeReview/Common/ArgparseAction.py @@ -0,0 +1,53 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### + +"""Module to implement argparse actions. + +""" + +#################################################################################################### + +__all__ = [ + 'PathAction', +] + +#################################################################################################### + +import argparse +from pathlib import Path + +#################################################################################################### + +class PathAction(argparse.Action): + + """Class to implement argparse action for path.""" + + ############################################## + + def __call__(self, parser, namespace, values, option_string=None): + + if values is not None: + if isinstance(values, list): + path = [Path(x) for x in values] + else: + path = Path(values) + else: + path = None + setattr(namespace, self.dest, path) diff --git a/CodeReview/Common/Platform2.py b/CodeReview/Common/Platform2.py new file mode 100644 index 0000000..ab23050 --- /dev/null +++ b/CodeReview/Common/Platform2.py @@ -0,0 +1,285 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### + +"""Module to query the platform for features. + +""" + +# Look alternative Python package + +#################################################################################################### + +from enum import Enum, auto +import os +import platform +import sys + +#################################################################################################### + +class PlatformType(Enum): + + Linux = auto() + Windows = auto() + OSX = auto() + +#################################################################################################### + +class Platform: + + """Class to store platform properties""" + + ############################################## + + def __init__(self): + + self.python_version = platform.python_version() + + self.os = self._get_os() + self.node = platform.node() + # deprecated in 3.8 see distro package + # self.distribution = ' '.join(platform.dist()) + self.machine = platform.machine() + self.architecture = platform.architecture()[0] + + # CPU + self.cpu = self._get_cpu() + self.number_of_cores = self._get_number_of_cores() + self.cpu_khz = self._get_cpu_khz() + self.cpu_mhz = int(self._get_cpu_khz()/float(1000)) # rint + + # RAM + self.memory_size_kb = self._get_memory_size_kb() + self.memory_size_mb = int(self.memory_size_kb/float(1024)) # rint + + ############################################## + + def _get_os(self): + + if os.name in ('nt',): + return PlatformType.Windows + elif sys.platform in ('linux',): + return PlatformType.Linux + # Fixme: + # elif sys.platform in 'osx': + # return PlatformType.OSX + else: + raise RuntimeError('unknown platform: {} / {}'.format(os.name, sys.platform)) + + ############################################## + + def _get_cpu(self): + + if self.os == PlatformType.Linux: + with open('/proc/cpuinfo', 'rt') as cpuinfo: + for line in cpuinfo: + if 'model name' in line: + s = line.split(':')[1] + return s.strip().rstrip() + + elif self.os == PlatformType.Windows: + raise NotImplementedError + + ############################################## + + def _get_number_of_cores(self): + + if self.os == PlatformType.Linux: + number_of_cores = 0 + with open('/proc/cpuinfo', 'rt') as cpuinfo: + for line in cpuinfo: + if 'processor' in line: + number_of_cores += 1 + return number_of_cores + + elif self.os == PlatformType.Windows: + return int(os.getenv('NUMBER_OF_PROCESSORS')) + + ############################################## + + def _get_cpu_khz(self): + + if self.os == PlatformType.Linux: + with open('/proc/cpuinfo', 'rt') as cpuinfo: + for line in cpuinfo: + if 'cpu MHz' in line: + s = line.split(':')[1] + return int(1000 * float(s)) + + if self.os == PlatformType.Windows: + raise NotImplementedError + + ############################################## + + def _get_memory_size_kb(self): + + if self.os == PlatformType.Linux: + with open('/proc/meminfo', 'rt') as cpuinfo: + for line in cpuinfo: + if 'MemTotal' in line: + s = line.split(':')[1][:-3] + return int(s) + + if self.os == PlatformType.Windows: + raise NotImplementedError + + ############################################## + + def __str__(self): + + str_template = ''' +Platform {0.node} + Hardware: + Machine: {0.machine} + Architecture: {0.architecture} + CPU: {0.cpu} + Number of Cores: {0.number_of_cores} + CPU Frequence: {0.cpu_mhz} MHz + Memory: {0.memory_size_mb} MB + + Python: {0.python_version} +''' + + return str_template.format(self) + +#################################################################################################### + +class QtPlatform(Platform): + + """Class to store Qt platform properties""" + + ############################################## + + def __init__(self): + + super().__init__() + + # Fixme: QT_VERSION_STR ... + from PyQt5 import QtCore, QtWidgets + # from QtShim import QtCore, QtWidgets + + self.qt_version = QtCore.QT_VERSION_STR + self.pyqt_version = QtCore.PYQT_VERSION_STR + + # Screen + + # try: + # application = QtWidgets.QApplication.instance() + # self.desktop = application.desktop() + # self.number_of_screens = self.desktop.screenCount() + # except: + # self.desktop = None + # self.number_of_screens = 0 + # for i in range(self.number_of_screens): + # self.screens.append(Screen(self, i)) + try: + application = QtWidgets.QApplication.instance() + self.screens = [Screen(screen) for screen in application.screens()] + except: + self.screens = [] + + # OpenGL + self.gl_renderer = None + self.gl_version = None + self.gl_vendor = None + self.gl_extensions = None + + ############################################## + + @property + def number_of_screens(self): + return len(self.screens) + + ############################################## + + def query_opengl(self): + + import OpenGL.GL as GL + + self.gl_renderer = GL.glGetString(GL.GL_RENDERER) + self.gl_version = GL.glGetString(GL.GL_VERSION) + self.gl_vendor = GL.glGetString(GL.GL_VENDOR) + self.gl_extensions = GL.glGetString(GL.GL_EXTENSIONS) + + ############################################## + + def __str__(self): + +# str_template = ''' +# OpenGL +# Render: {0.gl_renderer} +# Version: {0.gl_version} +# Vendor: {0.gl_vendor} +# Number of Screens: {0.number_of_screens} +# ''' +# message += str_template.format(self) + + message = super().__str__() + + for screen in self.screens: + message += str(screen) + + str_template = ''' + Software Versions: + Qt: {0.qt_version} + PyQt: {0.pyqt_version} +''' + message += str_template.format(self) + + return message + +#################################################################################################### + +class Screen: + + """Class to store screen properties""" + + ############################################## + + # def __init__(self, platform_obj, screen_id): + def __init__(self, qt_screen): + + # self.screen_id = screen_id + + # qt_screen_geometry = platform_obj.desktop.screenGeometry(screen_id) + # self.screen_width, self.screen_height = qt_screen_geometry.width(), qt_screen_geometry.height() + + # widget = platform_obj.desktop.screen(screen_id) + # self.dpi = widget.physicalDpiX(), widget.physicalDpiY() + + ## qt_available_geometry = self.desktop.availableGeometry(screen_id) + + self.name = qt_screen.name() + size = qt_screen.size() + self.screen_width, self.screen_height = size.width(), size.height() + self.dpi = qt_screen.physicalDotsPerInch() + self.dpi_x = qt_screen.physicalDotsPerInchX() + self.dpi_y = qt_screen.physicalDotsPerInchY() + + ############################################## + + def __str__(self): + + str_template = """ + Screen {0.name} + geometry {0.screen_width}x{0.screen_height} px + resolution {0.dpi:.2f} dpi +""" + + return str_template.format(self) diff --git a/CodeReview/QmlApplication/ApplicationMetadata.py b/CodeReview/QmlApplication/ApplicationMetadata.py new file mode 100644 index 0000000..076369a --- /dev/null +++ b/CodeReview/QmlApplication/ApplicationMetadata.py @@ -0,0 +1,69 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### + +__all__ = [ + 'ApplicationMetadata', +] + +#################################################################################################### + +from CodeReview import __version__ + +#################################################################################################### + +_about_message_template = ''' +

CodeReview

+ +

Version: {0.version}

+ +

Home Page: {0.url}

+ +

Copyright (C) {0.year} Fabrice Salvaire

+ +

Therms

+ +

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License along with this program. If not, see .

+''' + +#################################################################################################### + +class ApplicationMetadata: + + organisation_name = 'CodeReview' + organisation_domain = 'code-review.org' # Fixme: fake + + name = 'CodeReview' + display_name = 'CodeReview — A Code Review GUI' + + version = str(__version__) + + year = 2020 + + url = 'https://github.com/FabriceSalvaire/CodeReview' + + ############################################## + + @classmethod + def about_message(cls): + return _about_message_template.format(cls) diff --git a/CodeReview/QmlApplication/ApplicationSettings.py b/CodeReview/QmlApplication/ApplicationSettings.py new file mode 100644 index 0000000..c0b82fe --- /dev/null +++ b/CodeReview/QmlApplication/ApplicationSettings.py @@ -0,0 +1,173 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### + +"""Module to implement a application settings. + +""" + +#################################################################################################### + +__all__ = [ + 'ApplicationSettings', + 'Shortcut', +] + +#################################################################################################### + +import logging + +# Fixme: +from PyQt5.QtCore import QSettings +from PyQt5.QtQml import QQmlListProperty +from QtShim.QtCore import ( + Property, Signal, Slot, QObject, + # QUrl, +) + +from . import DefaultSettings +from .DefaultSettings import Shortcuts + +#################################################################################################### + +_module_logger = logging.getLogger(__name__) + +#################################################################################################### + +class Shortcut(QObject): + + _logger = _module_logger.getChild('Shortcut') + + ############################################## + + def __init__(self, settings, name, display_name, sequence): + + super().__init__() + + self._settings = settings + self._name = name + self._display_name = display_name + self._default_sequence = sequence + self._sequence = sequence + + ############################################## + + @Property(str, constant=True) + def name(self): + return self._name + + @Property(str, constant=True) + def display_name(self): + return self._display_name + + @Property(str, constant=True) + def default_sequence(self): + return self._default_sequence + + ############################################## + + sequence_changed = Signal() + + @Property(str, notify=sequence_changed) + def sequence(self): + self._logger.info('get sequence {} = {}'.format(self._name, self._sequence)) + return self._sequence + + @sequence.setter + def sequence(self, value): + if self._sequence != value: + self._logger.info('Shortcut {} = {}'.format(self._name, value)) + self._sequence = value + self._settings.set_shortcut(self) + self.sequence_changed.emit() + +#################################################################################################### + +class ApplicationSettings(QSettings): + + """Class to implement application settings.""" + + _logger = _module_logger.getChild('ApplicationSettings') + + ############################################## + + def __init__(self): + + super().__init__() + self._logger.info('Loading settings from {}'.format(self.fileName())) + + self._shortcut_map = { + name: Shortcut(self, name, self._shortcut_display_name(name), self._get_shortcut(name)) + for name in self._shortcut_names + } + self._shortcuts = list(self._shortcut_map.values()) + + ############################################## + + @property + def _shortcut_names(self): + return [name for name in dir(Shortcuts) if not name.startswith('_')] + + def _shortcut_display_name(self, name): + return getattr(Shortcuts, name)[0] + + def _default_shortcut(self, name): + return getattr(Shortcuts, name)[1] + + ############################################## + + def _shortcut_path(self, name): + return 'shortcut/{}'.format(name) + + ############################################## + + def _get_shortcut(self, name): + path = self._shortcut_path(name) + if self.contains(path): + return self.value(path) + else: + return self._default_shortcut(name) + + ############################################## + + def set_shortcut(self, shortcut): + path = self._shortcut_path(shortcut.name) + self.setValue(path, shortcut.sequence) + + ############################################## + + @Property(QQmlListProperty, constant=True) + def shortcuts(self): + return QQmlListProperty(Shortcut, self, self._shortcuts) + + ############################################## + + @Slot(str, result=Shortcut) + def shortcut(self, name): + return self._shortcut_map.get(name, None) + + ############################################## + + # @Slot(str, result=str) + # def shortcut_sequence(self, name): + # shortcut = self._shortcut_map.get(name, None) + # if shortcut is not None: + # return shortcut.sequence + # else: + # return None diff --git a/CodeReview/QmlApplication/DefaultSettings.py b/CodeReview/QmlApplication/DefaultSettings.py new file mode 100644 index 0000000..7763a83 --- /dev/null +++ b/CodeReview/QmlApplication/DefaultSettings.py @@ -0,0 +1,36 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### + +__all__ = [ + 'Shortcuts', +] + +#################################################################################################### + +from pathlib import Path + +from PyQt5.QtCore import QCoreApplication + +from CodeReview.Config import ConfigInstall + +#################################################################################################### + +class Shortcuts: + pass diff --git a/CodeReview/QmlApplication/KeySequenceEditor.py b/CodeReview/QmlApplication/KeySequenceEditor.py new file mode 100644 index 0000000..c132e6e --- /dev/null +++ b/CodeReview/QmlApplication/KeySequenceEditor.py @@ -0,0 +1,316 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### +# +# Translated from C++ to Python +# +# https://github.com/mitchcurtis/slate/blob/master/lib/keysequenceeditor.cpp +# https://github.com/mitchcurtis/slate/blob/master/lib/keysequenceeditor.h +# +# Copyright 2016, Mitch Curtis +# +# This file is part of Slate. +# +# Slate is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Slate is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Slate. If not, see . +# +#################################################################################################### + +#################################################################################################### + +import logging + +from PyQt5.QtCore import QMetaEnum +from PyQt5.QtGui import QKeyEvent, QKeySequence +from PyQt5.QtQuick import QQuickItem + +from QtShim.QtCore import ( + Property, Signal, Slot, QObject, + Qt, +) + +#################################################################################################### + +_module_logger = logging.getLogger(__name__) + +#################################################################################################### + +class KeyHelper(QObject): + def key_name(self, index): + key_enum_index = self.staticQtMetaObject.indexOfEnumerator('Key') + return self.staticQtMetaObject.enumerator(key_enum_index).valueToKey(index) + +_key_helper = KeyHelper() + +def _key_name(index): + # return _key_helper.key_name(index) # Fixme: PyQt5 issue + return index + +#################################################################################################### + +class KeySequenceEditor(QQuickItem): + + _logger = _module_logger.getChild('KeySequenceEditor') + + ############################################## + + def __init__(self, parent): + + super().__init__(parent) + + self._default_sequence = QKeySequence() # default sequence + self._edited_sequence = QKeySequence() # current/edited sequence + self._new_sequence = QKeySequence() # customised sequence + + self._reset_pressed_keys() + + ############################################## + + def _reset_pressed_keys(self): + self._logger.info('Clearing pressed keys') + self._pressed_keys = [] + + ############################################## + + default_sequence_changed = Signal() + + @Property(str, notify=default_sequence_changed) + def default_sequence(self): + return self._default_sequence.toString() + + ############################################## + + @default_sequence.setter + def default_sequence(self, default_sequence): + + if default_sequence != self._default_sequence.toString(): + self._default_sequence = QKeySequence(default_sequence, QKeySequence.PortableText) + self._set_edited_sequence('') # Fixme: why reset ??? + self.new_sequence = '' + self.default_sequence_changed.emit() + # This might not always be the case, I'm just lazy. + self.is_customised_changed.emit() + self.display_sequence_changed.emit() + + ############################################## + + new_sequence_changed = Signal() + + @Property(str, notify=new_sequence_changed) + def new_sequence(self): + return self._new_sequence.toString() + + @new_sequence.setter + def new_sequence(self, new_sequence): + + if new_sequence != self._new_sequence.toString(): + self._new_sequence = QKeySequence(new_sequence, QKeySequence.PortableText) + self._logger.info('Set new sequence to {}'.format(self._new_sequence.toString())) + self.new_sequence_changed.emit() + self.is_customised_changed.emit() + self.display_sequence_changed.emit() + + ############################################## + + display_sequence_changed = Signal() + + @Property(str, notify=display_sequence_changed) + def display_sequence(self): + + """Text to show in the sequence editor""" + + if self.hasActiveFocus(): + # we are editing the sequence + sequence = self._edited_sequence + elif self._new_sequence.isEmpty(): + # no new sequence + sequence = self._default_sequence + else: + sequence = self._new_sequence + + return sequence.toString() + + ############################################## + + is_customised_changed = Signal() + + @Property(bool, notify=is_customised_changed) + def is_customised(self): + """Flag to indicate a new valid sequence is set""" + return not self._new_sequence.isEmpty() and self._new_sequence != self._default_sequence + + ############################################## + + @Slot() + def reset(self): + """Reset the sequence to the default one""" + self._set_edited_sequence(self.default_sequence) + self.new_sequence = self.default_sequence + self._reset_pressed_keys() + + ############################################## + + def _set_edited_sequence(self, edited_sequence=''): + + """Update the edited sequence. + + emit *is_customised* and *display_sequence* + """ + + if edited_sequence != self._edited_sequence.toString(): + self._edited_sequence = QKeySequence(edited_sequence, QKeySequence.PortableText) + self._logger.info('Edited sequence changed to {}'.format(self._edited_sequence.toString())) + self.is_customised_changed.emit() + self.display_sequence_changed.emit() + + ############################################## + + def keyPressEvent(self, event): + + """Handler when a key is pressed. + + * use Escape to leave the control + * use Enter to accept the sequence + + """ + + if event.key() == Qt.Key_Escape: + self.setFocus(False) + + elif event.key() == Qt.Key_Return: + self._accept() + + elif not event.isAutoRepeat(): + modifiers = 0 + # event.modifiers().testFlag(...) + if event.modifiers() & Qt.ControlModifier: + modifiers |= Qt.CTRL + if event.modifiers() & Qt.ShiftModifier: + modifiers |= Qt.SHIFT + if event.modifiers() & Qt.AltModifier: + modifiers |= Qt.ALT + if event.modifiers() & Qt.MetaModifier: + modifiers |= Qt.META + + if Qt.Key_Shift <= event.key() <= Qt.Key_Meta: + self._logger.info('Only modifiers were pressed ({} / {} / {} ignoring'.format( + event.text(), + _key_name(event.key()), + QKeySequence(event.key()).toString() + )) + + else: + self._pressed_keys.append(event.key() | modifiers) + self._logger.info('Adding key {} / {} / {} with modifiers ({}) to pressed keys'.format( + event.text(), + _key_name(event.key()), + QKeySequence(event.key()).toString(), + # UnicodeEncodeError: 'utf-8' codec can't encode character '\udc21' in position 188: surrogates not allowed + QKeySequence(self._pressed_keys[-1]).toString(), + )) + + sequence = QKeySequence(*self._pressed_keys) # up to 4 keys + self._set_edited_sequence(sequence.toString()) + + if len(self._pressed_keys) == 4: + # That was the last key out of four possible keys end it here. + self._accept() + + event.accept() + + ############################################## + + def keyReleaseEvent(self, event): + event.accept() + + ############################################## + + def focusInEvent(self, event): + event.accept() + # The text displaying the shortcut should be cleared when editing begins. + self.display_sequence_changed.emit() + + ############################################## + + def focusOutEvent(self, event): + event.accept() + self._cancel() + + ############################################## + + def _accept(self): + + """Update *new_sequence* if the input is valid.""" + + self._logger.info('Attempting to accept input...') + + # If there hasn't been anything new successfully entered yet, check against the original + # sequence, otherwise check against the latest successfully entered sequence. + # Note: is_customised() assumes that an empty sequence isn't possible we might want to account + # for this in the future. + if ((self._edited_sequence != self._default_sequence) or + (self.is_customised and self._edited_sequence != self._new_sequence)): + if self._validate(self._edited_sequence): + self._logger.info('Input valid') + self.new_sequence = self._edited_sequence.toString() + else: + self._logger.info('Input invalid') + self._cancel() + else: + self._logger.info('Nothing has changed in the input') + # Nothing's changed. + + self._reset_pressed_keys() + self.setFocus(False) + + ############################################## + + def _cancel(self): + + self._reset_pressed_keys() + if self._edited_sequence.isEmpty(): + # If the edited sequence is empty, setting it to an empty string + # obviously won't change anything, and it will return early. + # We need the display sequence to update though, so call it here. + self.display_sequence_changed.emit() + else: + self._set_edited_sequence('') + + ############################################## + + def _validate(self, sequence): + + """Method to validate the new sequence""" + + self._logger.info('Validating key sequence {} ...'.format(sequence.toString())) + valid = True # False + # do some checks + return valid diff --git a/CodeReview/QmlApplication/QmlApplication.py b/CodeReview/QmlApplication/QmlApplication.py new file mode 100644 index 0000000..cffa0b5 --- /dev/null +++ b/CodeReview/QmlApplication/QmlApplication.py @@ -0,0 +1,405 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### + +"""Module to implement a Qt Application. + +""" + +#################################################################################################### + +__all__ = [ + 'QmlApplication', +] + +#################################################################################################### + +# import datetime +from pathlib import Path +import argparse +import logging +import sys +import traceback + +# Fixme: +from PyQt5 import QtCore +from PyQt5.QtWidgets import QApplication + +from QtShim.QtCore import ( + Property, Signal, Slot, QObject, + Qt, QTimer, QUrl +) +from QtShim.QtGui import QGuiApplication, QIcon +from QtShim.QtQml import qmlRegisterType, QQmlApplicationEngine +# Fixme: PYSIDE-574 qmlRegisterSingletonType and qmlRegisterUncreatableType missing in QtQml +from QtShim.QtQml import qmlRegisterUncreatableType +from QtShim.QtQuick import QQuickPaintedItem, QQuickView +# from QtShim.QtQuickControls2 import QQuickStyle + +from CodeReview.Common.ArgparseAction import PathAction +from CodeReview.Common.Platform2 import QtPlatform +from .ApplicationMetadata import ApplicationMetadata +from .ApplicationSettings import ApplicationSettings, Shortcut +from .KeySequenceEditor import KeySequenceEditor + +from .rcc import CodeReviewRessource + +#################################################################################################### + +_module_logger = logging.getLogger(__name__) + +#################################################################################################### + +class QmlApplication(QObject): + + """Class to implement a Qt QML Application.""" + + show_message = Signal(str) # message + show_error = Signal(str, str) # message backtrace + + _logger = _module_logger.getChild('QmlApplication') + + ############################################## + + def __init__(self, application): + + super().__init__() + + self._application = application + + ############################################## + + def notify_message(self ,message): + self.show_message.emit(str(message)) + + def notify_error(self, message): + backtrace_str = traceback.format_exc() + self.show_error.emit(str(message), backtrace_str) + + ############################################## + + @Property(str, constant=True) + def application_name(self): + return ApplicationMetadata.name + + @Property(str, constant=True) + def application_url(self): + return ApplicationMetadata.url + + @Property(str, constant=True) + def about_message(self): + return ApplicationMetadata.about_message() + +#################################################################################################### + +# Fixme: why not derive from QGuiApplication ??? +class Application(QObject): + + """Class to implement a Qt Application.""" + + instance = None + + _logger = _module_logger.getChild('Application') + + ############################################## + + # Fixme: Singleton + + @classmethod + def create(cls, *args, **kwargs): + + if cls.instance is not None: + raise NameError('Instance exists') + + cls.instance = cls(*args, **kwargs) + return cls.instance + + ############################################## + + def __init__(self): + + self._logger.info('Ctor') + + super().__init__() + + QtCore.qInstallMessageHandler(self._message_handler) + + self._parse_arguments() + + # For Qt Labs Platform native widgets + # self._application = QGuiApplication(sys.argv) + # use QCoreApplication::instance() to get instance + self._application = QApplication(sys.argv) + self._application.main = self + self._init_application() + + self._engine = QQmlApplicationEngine() + self._qml_application = QmlApplication(self) + self._application.qml_main = self._qml_application + + self._platform = QtPlatform() + # self._logger.info('\n' + str(self._platform)) + + #! self._load_translation() + self._register_qml_types() + self._set_context_properties() + self._load_qml_main() + + # self._run_before_event_loop() + + QTimer.singleShot(0, self._post_init) + + # self._view = QQuickView() + # self._view.setResizeMode(QQuickView.SizeRootObjectToView) + # self._view.setSource(qml_url) + + ############################################## + + @property + def args(self): + return self._args + + @property + def platform(self): + return self._platform + + @property + def settings(self): + return self._settings + + @property + def qml_application(self): + return self._qml_application + + ############################################## + + def _print_critical_message(self, message): + # print('\nCritical Error on {}'.format(datetime.datetime.now())) + # print('-'*80) + # print(message) + self._logger.critical(message) + + ############################################## + + def _message_handler(self, msg_type, context, msg): + + if msg_type == QtCore.QtDebugMsg: + method = self._logger.debug + elif msg_type == QtCore.QtInfoMsg: + method = self._logger.info + elif msg_type == QtCore.QtWarningMsg: + method = self._logger.warning + elif msg_type in (QtCore.QtCriticalMsg, QtCore.QtFatalMsg): + method = self._logger.critical + # method = None + + # local_msg = msg.toLocal8Bit() + # localMsg.constData() + context_file = context.file + if context_file is not None: + file_path = Path(context_file).name + else: + file_path = '' + message = '{1} {3} — {0}'.format(msg, file_path, context.line, context.function) + if method is not None: + method(message) + else: + self._print_critical_message(message) + + ############################################## + + def _on_critical_exception(self, exception): + message = str(exception) + '\n' + traceback.format_exc() + self._print_critical_message(message) + self._qml_application.notify_error(exception) + # sys.exit(1) + + ############################################## + + def _init_application(self): + + self._application.setOrganizationName(ApplicationMetadata.organisation_name) + self._application.setOrganizationDomain(ApplicationMetadata.organisation_domain) + + self._application.setApplicationName(ApplicationMetadata.name) + self._application.setApplicationDisplayName(ApplicationMetadata.display_name) + self._application.setApplicationVersion(ApplicationMetadata.version) + + logo_path = ':/icons/logo/logo-256.png' + self._application.setWindowIcon(QIcon(logo_path)) + + QIcon.setThemeName('material') + + self._settings = ApplicationSettings() + + ############################################## + + @classmethod + def setup_gui_application(self): + + # https://bugreports.qt.io/browse/QTBUG-55167 + # for path in ( + # 'qt.qpa.xcb.xcberror', + # ): + # QtCore.QCommon.LoggingCategory.setFilterRules('{} = false'.format(path)) + QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + + # QQuickStyle.setStyle('Material') + + ############################################## + + def _parse_arguments(self): + + parser = argparse.ArgumentParser( + description='CodeReview', + ) + + # parser.add_argument( + # '--version', + # action='store_true', default=False, + # help="show version and exit", + # ) + + parser.add_argument( + '--dont-translate', + action='store_true', + default=False, + help="Don't translate application", + ) + + parser.add_argument( + '--user-script', + action=PathAction, + default=None, + help='user script to execute', + ) + + parser.add_argument( + '--user-script-args', + default='', + help="user script args (don't forget to quote)", + ) + + self._args = parser.parse_args() + + ############################################## + + def _load_translation(self): + + if self._args.dont_translate: + return + + # Fixme: ConfigInstall + # directory = ':/translations' + directory = str(Path(__file__).parent.joinpath('rcc', 'translations')) + + locale = QtCore.QLocale() + self._translator = QtCore.QTranslator() + if self._translator.load(locale, 'code-review', '.', directory, '.qm'): + self._application.installTranslator(self._translator) + else: + raise NameError('No translator for locale {}'.format(locale.name())) + + ############################################## + + def _register_qml_types(self): + + qmlRegisterType(KeySequenceEditor, 'CodeReview', 1, 0, 'KeySequenceEditor') + + qmlRegisterUncreatableType(Shortcut, 'CodeReview', 1, 0, 'Shortcut', 'Cannot create Shortcut') + qmlRegisterUncreatableType(ApplicationSettings, 'CodeReview', 1, 0, 'ApplicationSettings', 'Cannot create ApplicationSettings') + qmlRegisterUncreatableType(QmlApplication, 'CodeReview', 1, 0, 'QmlApplication', 'Cannot create QmlApplication') + + ############################################## + + def _set_context_properties(self): + context = self._engine.rootContext() + context.setContextProperty('application', self._qml_application) + context.setContextProperty('application_settings', self._settings) + + ############################################## + + def _load_qml_main(self): + + self._logger.info('Load QML...') + + qml_path = Path(__file__).parent.joinpath('qml') + # qml_path = 'qrc:///qml' + self._engine.addImportPath(str(qml_path)) + + main_qml_path = qml_path.joinpath('main.qml') + self._qml_url = QUrl.fromLocalFile(str(main_qml_path)) + # QUrl('qrc:/qml/main.qml') + self._engine.objectCreated.connect(self._check_qml_is_loaded) + self._engine.load(self._qml_url) + + self._logger.info('QML loaded') + + ############################################## + + def _check_qml_is_loaded(self, obj, url): + # See https://bugreports.qt.io/browse/QTBUG-39469 + if (obj is None and url == self._qml_url): + sys.exit(-1) + + ############################################## + + def exec_(self): + # self._view.show() + self._logger.info('Start event loop') + sys.exit(self._application.exec_()) + + ############################################## + + def _post_init(self): + + # Fixme: ui refresh ??? + + self._logger.info('post Init...') + + if self._args.user_script is not None: + self.execute_user_script(self._args.user_script) + + self._logger.info('Post Init Done') + + ############################################## + + def execute_user_script(self, script_path): + + """Execute an user script provided by file *script_path* in a context where is defined a + variable *application* that is a reference to the application instance. + + """ + + script_path = Path(script_path).absolute() + self._logger.info('Execute user script:\n {}'.format(script_path)) + try: + source = open(script_path).read() + except FileNotFoundError: + self._logger.info('File {} not found'.format(script_path)) + sys.exit(1) + try: + bytecode = compile(source, script_path, 'exec') + except SyntaxError as exception: + self._on_critical_exception(exception) + try: + exec(bytecode, {'application':self}) + except Exception as exception: + self._on_critical_exception(exception) + self._logger.info('User script done') diff --git a/CodeReview/QmlApplication/__init__.py b/CodeReview/QmlApplication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CodeReview/QmlApplication/qml/Constants/Style.qml b/CodeReview/QmlApplication/qml/Constants/Style.qml new file mode 100644 index 0000000..965d85a --- /dev/null +++ b/CodeReview/QmlApplication/qml/Constants/Style.qml @@ -0,0 +1,74 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +// cf. http://wiki.qt.io/Qml_Styling + +// Fixme: lupdate +pragma Singleton +import QtQml 2.11 +import QtQuick 2.6 + +QtObject { + + property QtObject color: QtObject { + // property color primary: darken('#428bca', 0.65) + // property color success: '#5cb85c' + // property color info: '#5bc0de' + // property color warning: '#f0ad4e' + // property color danger: '#d9534f' + + property color primary: '#007bff' + property color success: '#28a745' + property color info: '#17a2b8' + property color warning: '#ffc107' + property color danger: '#dc3545' + + property color orange: '#e67e22' + + // "#5cb85c" hsv 120 128 184 + // "#f0ad4e" hsv 35 172 240 + } + + property QtObject font_size: QtObject { + property int tiny: 8 + property int small: 10 + property int base: 12 + property int large: 18 + property int huge: 20 + } + + property QtObject spacing: QtObject { + property int xs: 1 + property int small: 5 + property int base: 10 + property int large: 20 + property int huge: 30 + + property int xs_horizontal: 1 + property int small_horizontal: 5 + property int base_horizontal: 10 + property int large_horizontal: 20 + + property int xs_vertical: 1 + property int small_vertical: 5 + property int base_vertical: 10 + property int large_vertical: 20 + } +} diff --git a/CodeReview/QmlApplication/qml/Constants/qmldir b/CodeReview/QmlApplication/qml/Constants/qmldir new file mode 100644 index 0000000..f0a2972 --- /dev/null +++ b/CodeReview/QmlApplication/qml/Constants/qmldir @@ -0,0 +1,2 @@ +module Constants +singleton Style 1.0 Style.qml diff --git a/CodeReview/QmlApplication/qml/Controls/qmldir b/CodeReview/QmlApplication/qml/Controls/qmldir new file mode 100644 index 0000000..e9c2809 --- /dev/null +++ b/CodeReview/QmlApplication/qml/Controls/qmldir @@ -0,0 +1 @@ +module Controls diff --git a/CodeReview/QmlApplication/qml/UserInterface/Actions.qml b/CodeReview/QmlApplication/qml/UserInterface/Actions.qml new file mode 100644 index 0000000..b8dc38f --- /dev/null +++ b/CodeReview/QmlApplication/qml/UserInterface/Actions.qml @@ -0,0 +1,28 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 + +import CodeReview 1.0 + +Item { + id: root +} diff --git a/CodeReview/QmlApplication/qml/UserInterface/FooterToolBar.qml b/CodeReview/QmlApplication/qml/UserInterface/FooterToolBar.qml new file mode 100644 index 0000000..e68d214 --- /dev/null +++ b/CodeReview/QmlApplication/qml/UserInterface/FooterToolBar.qml @@ -0,0 +1,43 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.11 + +ToolBar { + id: root + + /******************************************************* + * + * API + * + */ + + property alias message: message_label.text + + /******************************************************/ + + RowLayout { + Label { + id: message_label + } + } +} diff --git a/CodeReview/QmlApplication/qml/UserInterface/HeaderToolBar.qml b/CodeReview/QmlApplication/qml/UserInterface/HeaderToolBar.qml new file mode 100644 index 0000000..272de45 --- /dev/null +++ b/CodeReview/QmlApplication/qml/UserInterface/HeaderToolBar.qml @@ -0,0 +1,44 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.11 + +import CodeReview 1.0 +import Widgets 1.0 as Widgets +import UserInterface 1.0 as Ui + +ToolBar { + id: root + + /******************************************************* + * + * API + * + */ + + /******************************************************/ + + RowLayout { + anchors.fill: parent + spacing: 10 + } +} diff --git a/CodeReview/QmlApplication/qml/UserInterface/MenuBar.qml b/CodeReview/QmlApplication/qml/UserInterface/MenuBar.qml new file mode 100644 index 0000000..f645b5f --- /dev/null +++ b/CodeReview/QmlApplication/qml/UserInterface/MenuBar.qml @@ -0,0 +1,84 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQml 2.2 +import QtQuick 2.11 +import QtQuick.Controls 2.4 + +import CodeReview 1.0 + +MenuBar { + id: root + + /******************************************************* + * + * API + * + */ + + property var about_dialog + property var options_dialog + // application_window.close_application() + + /******************************************************/ + + Action { + id: toggle_menu_bar_action + shortcut: 'm' + onTriggered: visible = !visible + } + + /******************************************************/ + + Menu { + title: qsTr('File') + + // onClosed: xxx.forceActiveFocus() + + MenuSeparator { } + + MenuItem { + icon.name: 'settings-black' + text: "Options" + onTriggered: options_dialog.open() + } + + MenuSeparator { } + + Action { + text: qsTr('Quit') + onTriggered: close_application() + } + } + + Menu { + title: qsTr('Help') + + Action { + text: qsTr('Documentation') + onTriggered: Qt.openUrlExternally(application.application_url) + } + + Action { + text: qsTr('About') + onTriggered: about_dialog.open() + } + } +} diff --git a/CodeReview/QmlApplication/qml/UserInterface/OptionsDialog.qml b/CodeReview/QmlApplication/qml/UserInterface/OptionsDialog.qml new file mode 100644 index 0000000..d2e0d73 --- /dev/null +++ b/CodeReview/QmlApplication/qml/UserInterface/OptionsDialog.qml @@ -0,0 +1,74 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQml.Models 2.2 +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.11 + +import CodeReview 1.0 +import Widgets 1.0 as Widgets + +Widgets.CentredDialog { + id: dialog + implicitWidth: 800 + implicitHeight: 400 + + standardButtons: Dialog.Ok | Dialog.Cancel + + // onAccepted: + // onRejected: + + /******************************************************/ + + header: TabBar { + id: tab_bar + + TabButton { + text: qsTr("General") + } + + TabButton { + text: qsTr("Shortcuts") + } + } + + StackLayout { + anchors.fill: parent + currentIndex: tab_bar.currentIndex + + Item { + id: shortcut_list_view_container + + ListView { + id: shortcut_list_view + anchors.fill: parent + clip: true + + //! model: application_settings.shortcuts + + delegate: Widgets.ShortcutRow { + width: parent.width + shortcut: modelData + } + } + } + } +} diff --git a/CodeReview/QmlApplication/qml/UserInterface/qmldir b/CodeReview/QmlApplication/qml/UserInterface/qmldir new file mode 100644 index 0000000..c6d287a --- /dev/null +++ b/CodeReview/QmlApplication/qml/UserInterface/qmldir @@ -0,0 +1,6 @@ +module Widgets +Actions 1.0 Actions.qml +FooterToolBar 1.0 FooterToolBar.qml +HeaderToolBar 1.0 HeaderToolBar.qml +MenuBar 1.0 MenuBar.qml +OptionsDialog 1.0 OptionsDialog.qml diff --git a/CodeReview/QmlApplication/qml/Widgets/AboutDialog.qml b/CodeReview/QmlApplication/qml/Widgets/AboutDialog.qml new file mode 100644 index 0000000..ae2cfe9 --- /dev/null +++ b/CodeReview/QmlApplication/qml/Widgets/AboutDialog.qml @@ -0,0 +1,49 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 + +import Widgets 1.0 as Widgets + +Widgets.CentredDialog { + + /****************************************************** + * + * API + * + */ + + property alias about_message: text_area.text + + /******************************************************/ + + id: dialog + modal: true + standardButtons: Dialog.Ok + + TextArea { + id: text_area + width: 800 + anchors.margins: 20 + wrapMode: TextEdit.Wrap + textFormat: TextEdit.RichText + } +} diff --git a/CodeReview/QmlApplication/qml/Widgets/CentredDialog.qml b/CodeReview/QmlApplication/qml/Widgets/CentredDialog.qml new file mode 100644 index 0000000..a7eded5 --- /dev/null +++ b/CodeReview/QmlApplication/qml/Widgets/CentredDialog.qml @@ -0,0 +1,29 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.12 + +Dialog { + modal: true + anchors.centerIn: parent + dim: true + // Fixme: cannot be resized +} diff --git a/CodeReview/QmlApplication/qml/Widgets/ErrorMessageDialog.qml b/CodeReview/QmlApplication/qml/Widgets/ErrorMessageDialog.qml new file mode 100644 index 0000000..b50454e --- /dev/null +++ b/CodeReview/QmlApplication/qml/Widgets/ErrorMessageDialog.qml @@ -0,0 +1,52 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 + +import Widgets 1.0 as Widgets + +Widgets.CentredDialog { + + /****************************************************** + * + * API + * + */ + + function open_with_message(message) { + text_area.text = message + open() + } + + /******************************************************/ + + id: dialog + modal: true + standardButtons: Dialog.Ok + + TextArea { + id: text_area + width: 800 + anchors.margins: 20 + wrapMode: TextEdit.Wrap + textFormat: TextEdit.RichText + } +} diff --git a/CodeReview/QmlApplication/qml/Widgets/MarkdownViewer.qml b/CodeReview/QmlApplication/qml/Widgets/MarkdownViewer.qml new file mode 100644 index 0000000..7bff8f8 --- /dev/null +++ b/CodeReview/QmlApplication/qml/Widgets/MarkdownViewer.qml @@ -0,0 +1,129 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.11 + +import Widgets 1.0 as Widgets +import Constants 1.0 + +Item { + + /****************************************************** + * + * API + * + */ + + property alias html_text: viewer.text + property alias markdown_text: editor.text + + signal markdown_text_edited() // Fixme: onMarkdown_textChanged ??? + + /******************************************************/ + + id: root + + /******************************************************/ + + Rectangle { + anchors.top: root.top + anchors.left: root.left + height: root.height + width: root.width - edit_button.width + // Fixme: + border.color: editor_container.visible ? Style.color.danger : '#ababac' + + ScrollView { + id: viewer_container + anchors.fill: parent + + TextArea { + id: viewer + wrapMode: TextEdit.Wrap + textFormat: TextEdit.RichText + readOnly: true + selectByMouse: true + + // background: Rectangle { + // } + } + + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + } + + ScrollView { + id: editor_container + anchors.fill: parent + visible: false + + TextArea { + id: editor + wrapMode: TextEdit.Wrap + textFormat: TextEdit.PlainText + selectByMouse: true + + // background: Rectangle { + // } + + Keys.onEscapePressed: { + focus = false + event.accepted = true + } + + onEditingFinished: { + markdown_text_edited() + // swap + editor_container.visible = false + viewer_container.visible = true + } + } + + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + } + } + + Widgets.WarnedToolButton { + id: edit_button + anchors.top: root.top + anchors.right: root.right + + icon.name: 'edit-black' + tip: qsTr('Edit Markdown') + icon.color: warned ? Style.color.danger : 'black' + + warned: editor.focus + + // When a user click on button while the editor has focus, signal order is + // 1) editor.onEditingFinished !!! + // 2) button.onFocuschanged + // 3) editor.onFocuschanged + // 4) button.onPressed + // 5) button.onClicked + + onClicked: { + // swap and set focus + viewer_container.visible = false + editor_container.visible = true + editor.focus = true + } + } +} diff --git a/CodeReview/QmlApplication/qml/Widgets/ShortcutRow.qml b/CodeReview/QmlApplication/qml/Widgets/ShortcutRow.qml new file mode 100644 index 0000000..6abbb81 --- /dev/null +++ b/CodeReview/QmlApplication/qml/Widgets/ShortcutRow.qml @@ -0,0 +1,105 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +// Forked from https://github.com/mitchcurtis/slate/blob/master/app/qml/ui/ShortcutRow.qml +// See KeySequenceEditor.py for item implementation + +import QtQuick 2.0 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.1 + +import CodeReview 1.0 + +RowLayout { + + /******************************************************* + * + * API + * + */ + + property var shortcut + + function reset() { + editor.reset() + } + + /******************************************************/ + + Label { + Layout.leftMargin: 10 + Layout.fillWidth: true + text: shortcut.display_name + } + + KeySequenceEditor { + id: editor + Layout.minimumWidth: 200 + implicitHeight: edit_button.implicitHeight + + // enabled: shortcut_name.length > 0 + default_sequence: shortcut.default_sequence + + // Fixme: name ... + onNew_sequenceChanged: { + console.info('New sequence', editor.new_sequence) + shortcut.sequence = editor.new_sequence + } + + // The fix for QTBUG-57098 probably should have been implemented in C++ as well. + // I've tried implementing it in C++ with event() and converting the event + // to a QKeyEvent when the type is ShortcutOverride, but it didn't work. + Keys.onShortcutOverride: event.accepted = (event.key === Qt.Key_Escape) + + ItemDelegate { + id: edit_button + width: parent.width + implicitWidth: 200 + text: editor.display_sequence + font.bold: editor.is_customised + + onClicked: editor.forceActiveFocus() + + // Animation to blink the sequence while editing + SequentialAnimation { + id: flash_animation + running: editor.activeFocus + + loops: Animation.Infinite + alwaysRunToEnd: true + + NumberAnimation { + target: edit_button.contentItem + property: 'opacity' + from: 1 + to: 0.5 + duration: 300 + } + NumberAnimation { + target: edit_button.contentItem + property: 'opacity' + from: 0.5 + to: 1 + duration: 300 + } + } + } + } +} diff --git a/CodeReview/QmlApplication/qml/Widgets/qmldir b/CodeReview/QmlApplication/qml/Widgets/qmldir new file mode 100644 index 0000000..46adc9b --- /dev/null +++ b/CodeReview/QmlApplication/qml/Widgets/qmldir @@ -0,0 +1,6 @@ +module Widgets +AboutDialog 1.0 AboutDialog.qml +CentredDialog 1.0 CentredDialog.qml +ErrorMessageDialog 1.0 ErrorMessageDialog.qml +MarkdownViewer 1.0 MarkdownViewer.qml +ShortcutRow 1.0 ShortcutRow.qml diff --git a/CodeReview/QmlApplication/qml/main.qml b/CodeReview/QmlApplication/qml/main.qml new file mode 100644 index 0000000..af93723 --- /dev/null +++ b/CodeReview/QmlApplication/qml/main.qml @@ -0,0 +1,173 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2019 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.11 + +import CodeReview 1.0 +import Widgets 1.0 as Widgets +import UserInterface 1.0 as Ui + +ApplicationWindow { + id: application_window + + /******************************************************* + * + * API + * + */ + + property var shortcuts: null + + function close_application(close) { + console.info('Close application') + show_message(qsTr('Close ...')) + if (!close) + Qt.quit() + // else + // close.accepted = false + } + + function clear_message() { + footer_tool_bar.message = '' + } + + function show_message(message) { + footer_tool_bar.message = message + } + + /******************************************************* + * + * + */ + + title: qsTr('Code Review') // Fixme: ??? + visible: true + width: 1000 + height: 500 + + Component.onCompleted: { + console.info('ApplicationWindow.onCompleted') + application.show_message.connect(on_message) + application.show_error.connect(on_error) + application_window.showMaximized() + + // Fixme: prevent crash when opening option dialog + // RuntimeError: wrapped C/C++ object of type Shortcut has been deleted + let _shortcuts = application_settings.shortcuts + shortcuts = {} // [] + /* for (var shortcut in _shortcuts) */ + /* console.info('shortcut', shortcut) */ + for (var i = 0; i < _shortcuts.length; i++) { + var shortcut = _shortcuts[i] + shortcuts[shortcut.name] = shortcut + // shortcuts.push(shortcut) + } + } + + function on_message(message) { + error_message_dialog.open_with_message(message) + } + + function on_error(message, backtrace) { + var text = message + '\n' + backtrace + error_message_dialog.open_with_message(text) + } + + /******************************************************* + * + * Slots + * + */ + + onClosing: close_application(close) + + /******************************************************* + * + * Dialogs + * + */ + + Widgets.AboutDialog { + id: about_dialog + title: qsTr('About Code Review') + about_message: application.about_message // qsTr('...') + } + + Widgets.ErrorMessageDialog { + id: error_message_dialog + title: qsTr('An error occurred in Code Review') + } + + Ui.OptionsDialog { + id: options_dialog + } + + /******************************************************* + * + * Actions + * + */ + + Ui.Actions { + id: actions + } + + /******************************************************* + * + * Menu + * + */ + + // Fixme: use native menu ??? + menuBar: Ui.MenuBar { + id: menu_bar + about_dialog: about_dialog + options_dialog: options_dialog + } + + /******************************************************* + * + * Header + * + */ + + /* header: Ui.HeaderToolBar { */ + /* id: header_tool_bar */ + /* actions: actions */ + /* } */ + + /******************************************************* + * + * Items + * + */ + + /******************************************************* + * + * Footer + * + */ + + footer: Ui.FooterToolBar { + id: footer_tool_bar + } +} diff --git a/CodeReview/QmlApplication/rcc/Makefile b/CodeReview/QmlApplication/rcc/Makefile new file mode 120000 index 0000000..b991ffc --- /dev/null +++ b/CodeReview/QmlApplication/rcc/Makefile @@ -0,0 +1 @@ +../../../tasks/Makefile \ No newline at end of file diff --git a/CodeReview/QmlApplication/rcc/__init__.py b/CodeReview/QmlApplication/rcc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CodeReview/QmlApplication/rcc/code-review.qrc b/CodeReview/QmlApplication/rcc/code-review.qrc new file mode 100644 index 0000000..0d2191e --- /dev/null +++ b/CodeReview/QmlApplication/rcc/code-review.qrc @@ -0,0 +1,17 @@ + + + + qtquickcontrols2.conf + + + + + + + + + + + + + diff --git a/CodeReview/QmlApplication/rcc/qtquickcontrols2.conf b/CodeReview/QmlApplication/rcc/qtquickcontrols2.conf new file mode 100644 index 0000000..9cd1411 --- /dev/null +++ b/CodeReview/QmlApplication/rcc/qtquickcontrols2.conf @@ -0,0 +1,2 @@ +[Controls] +Style=Fusion diff --git a/CodeReview/__init__.py b/CodeReview/__init__.py index e69de29..33c3259 100644 --- a/CodeReview/__init__.py +++ b/CodeReview/__init__.py @@ -0,0 +1 @@ +__version__ = '0.3.0' # Fixme: for QML diff --git a/bin/pyqgit-qml b/bin/pyqgit-qml new file mode 100755 index 0000000..bb317ea --- /dev/null +++ b/bin/pyqgit-qml @@ -0,0 +1,37 @@ +#! /usr/bin/env python3 + +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2020 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +import CodeReview.Common.Logging.Logging as Logging +logger = Logging.setup_logging('pyqgit') + +#################################################################################################### + +from CodeReview.QmlApplication.QmlApplication import Application + +#################################################################################################### + +# application = Application() +Application.setup_gui_application() +application = Application.create() +application.exec_() From c44afe63183e0166c76a99b743b3f0aa834d6e94 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 18:59:23 +0200 Subject: [PATCH 22/69] refactor ui --- CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index 954ecf5..256719c 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -135,6 +135,11 @@ def _init_ui(self): bottom_horizontal_layout = QtWidgets.QHBoxLayout() bottom_widget.setLayout(bottom_horizontal_layout) + vertical_layout = QtWidgets.QVBoxLayout() + bottom_horizontal_layout.addLayout(vertical_layout) + self._commit_table = QtWidgets.QTableView() + vertical_layout.addWidget(self._commit_table) + vertical_layout = QtWidgets.QVBoxLayout() bottom_horizontal_layout.addLayout(vertical_layout) self._commit_sha = QtWidgets.QLineEdit() @@ -151,11 +156,6 @@ def _init_ui(self): parent.setReadOnly(True) horizontal_layout.addWidget(parent) self._parent_labels.append(parent) - self._commit_table = QtWidgets.QTableView() - vertical_layout.addWidget(self._commit_table) - - vertical_layout = QtWidgets.QVBoxLayout() - bottom_horizontal_layout.addLayout(vertical_layout) self._review_comment = QtWidgets.QTextEdit() vertical_layout.addWidget(self._review_comment) horizontal_layout = QtWidgets.QHBoxLayout() From b3bebd758972fa477d83506112b620f0d8628744 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 23:03:28 +0200 Subject: [PATCH 23/69] implement diff ab/ --- .../GUI/LogBrowser/LogBrowserMainWindow.py | 61 +++++++++++++++++-- share/icons/32x32/edit-delete.svg | 1 + share/icons/32x32/media-playlist-repeat.svg | 17 ++++++ share/icons/svg/edit-delete.svg | 1 + share/icons/svg/media-playlist-repeat.svg | 1 + 5 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 share/icons/32x32/edit-delete.svg create mode 100644 share/icons/32x32/media-playlist-repeat.svg create mode 120000 share/icons/svg/edit-delete.svg create mode 120000 share/icons/svg/media-playlist-repeat.svg diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index 256719c..be25d8a 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -52,8 +52,8 @@ def __init__(self, parent=None): super(LogBrowserMainWindow, self).__init__(title='CodeReview Log Browser', parent=parent) - icon_loader = IconLoader() - self.setWindowIcon(icon_loader['code-review@svg']) + self._icon_loader = IconLoader() + self.setWindowIcon(self._icon_loader['code-review@svg']) self._current_revision = None self._diff = None @@ -88,6 +88,31 @@ def _init_ui(self): vertical_layout = QtWidgets.QVBoxLayout() horizontal_layout.addLayout(vertical_layout) + + horizontal_layout2 = QtWidgets.QHBoxLayout() + vertical_layout.addLayout(horizontal_layout2) + button = QtWidgets.QPushButton('Diff') + button.clicked.connect(self._on_diff_ab) + horizontal_layout2.addWidget(button) + button = QtWidgets.QPushButton() + button.setIcon(self._icon_loader['edit-delete@svg']) + horizontal_layout2.addWidget(button) + self._diff_a = QtWidgets.QLineEdit(self) + button.clicked.connect(lambda: self._diff_a.clear()) + horizontal_layout2.addWidget(self._diff_a) + button = QtWidgets.QPushButton() + button.setIcon(self._icon_loader['media-playlist-repeat@svg']) + button.clicked.connect(self._on_diff_exchange) + horizontal_layout2.addWidget(button) + self._diff_b = QtWidgets.QLineEdit(self) + horizontal_layout2.addWidget(self._diff_b) + button = QtWidgets.QPushButton() + button.setIcon(self._icon_loader['edit-delete@svg']) + button.clicked.connect(lambda: self._diff_b.clear()) + horizontal_layout2.addWidget(button) + # for widget in (self._diff_a, self._diff_b): + # widget.editingFinished.connect(self._on_diff_ab) + self._branch_name = QtWidgets.QLineEdit(self) self._branch_name.setReadOnly(True) vertical_layout.addWidget(self._branch_name) @@ -195,8 +220,6 @@ def finish_table_connections(self): def _create_actions(self): - icon_loader = IconLoader() - self._stagged_mode_action = \ QtWidgets.QAction('Stagged', self, @@ -231,7 +254,7 @@ def _create_actions(self): self._all_change_mode_action.setChecked(True) self._reload_action = \ - QtWidgets.QAction(icon_loader['view-refresh@svg'], + QtWidgets.QAction(self._icon_loader['view-refresh@svg'], 'Refresh', self, toolTip='Refresh', @@ -588,3 +611,31 @@ def _on_go_clicked(self, parent_index): self._log_table.selectRow(index.row()) except IndexError: pass + + ############################################## + + def _on_diff_exchange(self): + diff_a = self._diff_a.text() + diff_b = self._diff_b.text() + self._diff_a.setText(diff_b) + self._diff_b.setText(diff_a) + self._on_diff_ab() + + ############################################## + + def _on_diff_ab(self): + + diff_a = self._diff_a.text() + diff_b = self._diff_b.text() + + if diff_a and diff_b: + try: + kwargs = dict(a=diff_a, b=diff_b) + self._diff = self._application.repository.diff(**kwargs) + + commit_table_model = self._commit_table.model() + commit_table_model.update(self._diff) + self._commit_table.resizeColumnsToContents() + except ValueError as e: + self._logger.warning(e) + self.show_message(str(e)) diff --git a/share/icons/32x32/edit-delete.svg b/share/icons/32x32/edit-delete.svg new file mode 100644 index 0000000..0a62e5b --- /dev/null +++ b/share/icons/32x32/edit-delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/32x32/media-playlist-repeat.svg b/share/icons/32x32/media-playlist-repeat.svg new file mode 100644 index 0000000..0e3071b --- /dev/null +++ b/share/icons/32x32/media-playlist-repeat.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/share/icons/svg/edit-delete.svg b/share/icons/svg/edit-delete.svg new file mode 120000 index 0000000..39d3d8a --- /dev/null +++ b/share/icons/svg/edit-delete.svg @@ -0,0 +1 @@ +../32x32/edit-delete.svg \ No newline at end of file diff --git a/share/icons/svg/media-playlist-repeat.svg b/share/icons/svg/media-playlist-repeat.svg new file mode 120000 index 0000000..47a4244 --- /dev/null +++ b/share/icons/svg/media-playlist-repeat.svg @@ -0,0 +1 @@ +../32x32/media-playlist-repeat.svg \ No newline at end of file From 8bd4c83ffde995614ed96ab1fa162aa6bab6afe8 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 23:05:19 +0200 Subject: [PATCH 24/69] gitignore --- .gitignore | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 486ec80..82a25f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,21 @@ *~ __pycache__ .directory -# + .cache/ CodeReview.egg-info/ -CodeReview/PatienceDiff/_patiencediff_c.cpython-36m-x86_64-linux-gnu.so +CodeReview/PatienceDiff/_patiencediff_c.cpython-* +CodeReview/QmlApplication/rcc/CodeReviewRessource.py +CodeReview/QmlApplication/rcc/code-review.rcc MANIFEST build/ dist/ doc/sphinx/build/ doc/sphinx/source/api/ -# + bzr-backup/ trash/ -# + CodeReview/TextDistance/buggy.c DELETED-SPHINX-SOURCE du.log @@ -38,3 +40,7 @@ test/test-repo/ test/token.txt tools/rsync-vps.sh tools/upload-www + +__BUGS__/ +__TRASH__/ +review.json From dc21cc0febfd298ee028615655ff018ce67c1bb5 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 23:05:28 +0200 Subject: [PATCH 25/69] code-review.desktop --- distro/fedora/code-review.desktop | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 distro/fedora/code-review.desktop diff --git a/distro/fedora/code-review.desktop b/distro/fedora/code-review.desktop new file mode 100644 index 0000000..359e33e --- /dev/null +++ b/distro/fedora/code-review.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] + +Encoding=UTF-8 +Name=code-review +Comment=... +Icon=code-review +Exec=code-review +Terminal=false +Type=Application +Categories=...; +Comment[fr]=... From e7e8feb81363171f52658e386935dfcb3ebd1204 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Tue, 12 May 2020 23:09:17 +0200 Subject: [PATCH 26/69] cleanup --- CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py index be25d8a..560cc96 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserMainWindow.py @@ -388,6 +388,8 @@ def _update_commit_table(self, index=None): else: index = 0 + # Diff a=old b=new commit + if index: self._current_revision = index # log_table_model = self._log_table.model() @@ -407,11 +409,8 @@ def _update_commit_table(self, index=None): self._review_comment.setText(self._review_note.text) else: self._review_note = ReviewNote(sha) - # try: - commit_a = self._current_commit.parents[0] - kwargs = dict(a=commit_a, b=self._current_commit) # Fixme: - # except IndexError: - # kwargs = dict(a=self._current_commit) + commit_a = self._current_commit.parents[0] # take first parent + kwargs = dict(a=commit_a, b=self._current_commit) else: # working directory self._current_revision = None From e25338972cf5df74f22fd1382695a3402b28d260 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Wed, 13 May 2020 00:16:07 +0200 Subject: [PATCH 27/69] add diff-a/b arg --- CodeReview/GUI/LogBrowser/LogBrowserApplication.py | 4 ++++ bin/pyqgit | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py index ca8eb9c..c302e6f 100644 --- a/CodeReview/GUI/LogBrowser/LogBrowserApplication.py +++ b/CodeReview/GUI/LogBrowser/LogBrowserApplication.py @@ -72,6 +72,10 @@ def post_init(self): self._init_file_system_watcher() self._main_window.show_working_tree_diff() + self._main_window._diff_a.setText(self._args.diff_a) + self._main_window._diff_b.setText(self._args.diff_b) + self._main_window._on_diff_ab() + ############################################## def show_message(self, message=None, timeout=0, warn=False): diff --git a/bin/pyqgit b/bin/pyqgit index 7160f96..82d9aab 100755 --- a/bin/pyqgit +++ b/bin/pyqgit @@ -52,6 +52,18 @@ argument_parser.add_argument( help='path', ) +argument_parser.add_argument( + '--diff-a', + default=None, + help='set a/old reference for diff a/b', +) + +argument_parser.add_argument( + '--diff-b', + default=None, + help='set b/new reference for diff a/b', +) + argument_parser.add_argument( '--user-script', action=PathAction, From d218f4ba19e9c49bdf0358ddf4d23cf7893abd2e Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Wed, 13 May 2020 19:53:21 +0200 Subject: [PATCH 28/69] fix yaml loader --- CodeReview/Common/Logging/Logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeReview/Common/Logging/Logging.py b/CodeReview/Common/Logging/Logging.py index ff9ae0d..a717560 100644 --- a/CodeReview/Common/Logging/Logging.py +++ b/CodeReview/Common/Logging/Logging.py @@ -49,7 +49,7 @@ def __init__(self, context, stderr=True): def setup_logging(application_name, config_file=ConfigInstall.Logging.default_config_file): logging_config_file_name = ConfigInstall.Logging.find(config_file) - logging_config = yaml.load(open(logging_config_file_name, 'r')) + logging_config = yaml.load(open(logging_config_file_name, 'r'), Loader=yaml.SafeLoader) # Fixme: \033 is not interpreted in YAML formatter_config = logging_config['formatters']['ansi']['format'] From 2e3dc50aa0498b186c22e1bfd69d104308cb4fb6 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Wed, 13 May 2020 19:54:15 +0200 Subject: [PATCH 29/69] implement QmlRepository --- CodeReview/GUI/LogBrowser/LogTableModel.py | 2 +- ...mlApplication.py => QmlBaseApplication.py} | 70 ++-- .../QmlApplication/QmlMainApplication.py | 138 +++++++ CodeReview/QmlApplication/QmlRepository.py | 352 ++++++++++++++++++ .../QmlApplication/qml/Widgets/BranchList.qml | 44 +++ .../QmlApplication/qml/Widgets/CommitLog.qml | 44 +++ .../QmlApplication/qml/Widgets/TagList.qml | 44 +++ CodeReview/QmlApplication/qml/Widgets/qmldir | 3 + CodeReview/QmlApplication/qml/main.qml | 53 +++ CodeReview/Repository/Git.py | 167 +++++---- bin/pyqgit-qml | 2 +- 11 files changed, 821 insertions(+), 98 deletions(-) rename CodeReview/QmlApplication/{QmlApplication.py => QmlBaseApplication.py} (90%) create mode 100644 CodeReview/QmlApplication/QmlMainApplication.py create mode 100644 CodeReview/QmlApplication/QmlRepository.py create mode 100644 CodeReview/QmlApplication/qml/Widgets/BranchList.qml create mode 100644 CodeReview/QmlApplication/qml/Widgets/CommitLog.qml create mode 100644 CodeReview/QmlApplication/qml/Widgets/TagList.qml diff --git a/CodeReview/GUI/LogBrowser/LogTableModel.py b/CodeReview/GUI/LogBrowser/LogTableModel.py index 0447b17..62264d4 100644 --- a/CodeReview/GUI/LogBrowser/LogTableModel.py +++ b/CodeReview/GUI/LogBrowser/LogTableModel.py @@ -82,7 +82,7 @@ def __init__(self, repository): super().__init__() self._tags = repository.tags - commits = repository.commits + commits = repository.commits_for_head self._number_of_rows = len(commits) self._rows = [('', 'Working directory changes', '', '', None)] for i, commit in enumerate(commits): diff --git a/CodeReview/QmlApplication/QmlApplication.py b/CodeReview/QmlApplication/QmlBaseApplication.py similarity index 90% rename from CodeReview/QmlApplication/QmlApplication.py rename to CodeReview/QmlApplication/QmlBaseApplication.py index cffa0b5..a28fa34 100644 --- a/CodeReview/QmlApplication/QmlApplication.py +++ b/CodeReview/QmlApplication/QmlBaseApplication.py @@ -1,4 +1,4 @@ -#################################################################################################### +################################################################################################### # # CodeReview - A Code Review GUI # Copyright (C) 2019 Fabrice Salvaire @@ -18,19 +18,12 @@ # #################################################################################################### -"""Module to implement a Qt Application. - -""" - #################################################################################################### -__all__ = [ - 'QmlApplication', -] +__all__ = ['QmlApplication', 'Application'] #################################################################################################### -# import datetime from pathlib import Path import argparse import logging @@ -115,6 +108,8 @@ class Application(QObject): instance = None + QmlApplication_CLS = QmlApplication + _logger = _module_logger.getChild('Application') ############################################## @@ -150,27 +145,29 @@ def __init__(self): self._init_application() self._engine = QQmlApplicationEngine() - self._qml_application = QmlApplication(self) + self._qml_application = self.QmlApplication_CLS(self) self._application.qml_main = self._qml_application self._platform = QtPlatform() # self._logger.info('\n' + str(self._platform)) #! self._load_translation() - self._register_qml_types() + self.register_qml_types() self._set_context_properties() self._load_qml_main() # self._run_before_event_loop() - QTimer.singleShot(0, self._post_init) + self._application.aboutToQuit.connect(self.aboutToQuit) - # self._view = QQuickView() - # self._view.setResizeMode(QQuickView.SizeRootObjectToView) - # self._view.setSource(qml_url) + QTimer.singleShot(0, self.post_init) ############################################## + @property + def parser(self): + return self._parser + @property def args(self): return self._args @@ -253,51 +250,51 @@ def _init_application(self): @classmethod def setup_gui_application(self): - # https://bugreports.qt.io/browse/QTBUG-55167 - # for path in ( - # 'qt.qpa.xcb.xcberror', - # ): - # QtCore.QCommon.LoggingCategory.setFilterRules('{} = false'.format(path)) QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling) - # QQuickStyle.setStyle('Material') ############################################## def _parse_arguments(self): - parser = argparse.ArgumentParser( + self._parser = argparse.ArgumentParser( description='CodeReview', ) - # parser.add_argument( + self.parse_arguments() + + self._args = self._parser.parse_args() + + ############################################## + + def parse_arguments(self): + + # self.parser.add_argument( # '--version', # action='store_true', default=False, # help="show version and exit", # ) - parser.add_argument( + self.parser.add_argument( '--dont-translate', action='store_true', default=False, help="Don't translate application", ) - parser.add_argument( + self.parser.add_argument( '--user-script', action=PathAction, default=None, help='user script to execute', ) - parser.add_argument( + self.parser.add_argument( '--user-script-args', default='', help="user script args (don't forget to quote)", ) - self._args = parser.parse_args() - ############################################## def _load_translation(self): @@ -318,13 +315,13 @@ def _load_translation(self): ############################################## - def _register_qml_types(self): + def register_qml_types(self): qmlRegisterType(KeySequenceEditor, 'CodeReview', 1, 0, 'KeySequenceEditor') qmlRegisterUncreatableType(Shortcut, 'CodeReview', 1, 0, 'Shortcut', 'Cannot create Shortcut') qmlRegisterUncreatableType(ApplicationSettings, 'CodeReview', 1, 0, 'ApplicationSettings', 'Cannot create ApplicationSettings') - qmlRegisterUncreatableType(QmlApplication, 'CodeReview', 1, 0, 'QmlApplication', 'Cannot create QmlApplication') + qmlRegisterUncreatableType(self.QmlApplication_CLS, 'CodeReview', 1, 0, 'QmlApplication', 'Cannot create QmlApplication') ############################################## @@ -361,13 +358,20 @@ def _check_qml_is_loaded(self, obj, url): ############################################## def exec_(self): - # self._view.show() self._logger.info('Start event loop') - sys.exit(self._application.exec_()) + rc = self._application.exec_() + self._logger.info('Event loop done {}'.format(rc)) + del self._engine # solve quit issue ? + sys.exit(rc) + + ############################################## + + def aboutToQuit(self): + self._logger.info('') ############################################## - def _post_init(self): + def post_init(self): # Fixme: ui refresh ??? diff --git a/CodeReview/QmlApplication/QmlMainApplication.py b/CodeReview/QmlApplication/QmlMainApplication.py new file mode 100644 index 0000000..ca8a530 --- /dev/null +++ b/CodeReview/QmlApplication/QmlMainApplication.py @@ -0,0 +1,138 @@ +#################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +__all__ = ['Application'] + +#################################################################################################### + +import logging + +from QtShim.QtCore import ( + Property, Signal, Slot, QObject, + Qt, QTimer, QUrl +) + +# Fixme: PYSIDE-574 qmlRegisterSingletonType and qmlRegisterUncreatableType missing in QtQml +from QtShim.QtQml import qmlRegisterUncreatableType + +from . import QmlBaseApplication +from ..Common.ArgparseAction import PathAction +from .QmlRepository import ( + QmlBranch, + QmlCommit, + QmlCommitPool, + QmlNote, + QmlReference, + QmlRepository, +) + +#################################################################################################### + +_module_logger = logging.getLogger(__name__) + +#################################################################################################### + +class QmlApplication(QmlBaseApplication.QmlApplication): + + _logger = _module_logger.getChild('QmlApplication') + + ############################################## + + def __init__(self, application): + + super().__init__(application) + + self._repository = None + + ############################################## + + @Slot(str) + def load_repository(self, path): + + self._logger.info(path) + try: + self._repository = QmlRepository(path) + except NameError as exception: + # self.show_message(, warn=True) + self._repository = None + pass + self.repository_changed.emit() + + ############################################## + + repository_changed = Signal() + + @Property(QmlRepository, notify=repository_changed) + def repository(self): + return self._repository + +#################################################################################################### + +class Application(QmlBaseApplication.Application): + + _logger = _module_logger.getChild('Application') + + QmlApplication_CLS = QmlApplication + + ############################################## + + def parse_arguments(self): + + super().parse_arguments() + + self.parser.add_argument( + 'path', metavar='PATH', + action=PathAction, + nargs='?', default='.', + help='path', + ) + + self.parser.add_argument( + '--diff-a', + default=None, + help='set a/old reference for diff a/b', + ) + + self.parser.add_argument( + '--diff-b', + default=None, + help='set b/new reference for diff a/b', + ) + + ############################################## + + def register_qml_types(self): + + super().register_qml_types() + + qmlRegisterUncreatableType(QmlBranch, 'CodeReview', 1, 0, 'QmlBranch', 'Cannot create QmlBranch') + qmlRegisterUncreatableType(QmlCommit, 'CodeReview', 1, 0, 'QmlCommit', 'Cannot create QmlCommit') + qmlRegisterUncreatableType(QmlCommitPool, 'CodeReview', 1, 0, 'QmlCommitPool', 'Cannot create QmlCommitPool') + qmlRegisterUncreatableType(QmlNote, 'CodeReview', 1, 0, 'QmlNote', 'Cannot create QmlNote') + qmlRegisterUncreatableType(QmlReference, 'CodeReview', 1, 0, 'QmlReference', 'Cannot create QmlReference') + qmlRegisterUncreatableType(QmlRepository, 'CodeReview', 1, 0, 'QmlRepository', 'Cannot create QmlRepository') + + ############################################## + + def post_init(self): + self.qml_application.load_repository(self.args.path) + super().post_init() diff --git a/CodeReview/QmlApplication/QmlRepository.py b/CodeReview/QmlApplication/QmlRepository.py new file mode 100644 index 0000000..ff718a6 --- /dev/null +++ b/CodeReview/QmlApplication/QmlRepository.py @@ -0,0 +1,352 @@ +################################################################################################### +# +# CodeReview - A Code Review GUI +# Copyright (C) 2019 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +__all__ = [ + 'QmlBranch', + 'QmlCommit' + 'QmlCommitPool', + 'QmlNote', + 'QmlReference', + 'QmlRepository', +] + +#################################################################################################### + +# import re +import datetime +import logging +import os + +fromtimestamp = datetime.datetime.fromtimestamp + +from PyQt5.QtQml import QQmlListProperty +from QtShim.QtCore import ( + Property, Signal, Slot, QObject, +) + +from CodeReview.Repository.Git import pygit, RepositoryNotFound, GitRepository + +#################################################################################################### + +_module_logger = logging.getLogger(__name__) + + +#################################################################################################### + +class QmlRepositoryChild(QObject): + + ############################################## + + def __init__(self, repository): + # repository: QmlRepository + super().__init__() + self._repository = repository + +#################################################################################################### + +class QmlBranch(QmlRepositoryChild): + + # https://www.pygit2.org/branches.html#the-branch-type + + ############################################## + + def __init__(self, repository, branch: pygit.Branch): + super().__init__(repository) + self._branch = branch + + ############################################## + + @Property(str, constant=True) + def name(self): + return self._branch.name + + ############################################## + + @Property(bool) + def is_checked_out(self): + """ True if branch is checked out by any repo connected to the current one, False otherwise.""" + return self._branch.is_checked_out + + @Property(bool) + def is_head(self): + """True if HEAD points at the branch, False otherwise.""" + return self._branch.is_head + + @Property(str) + def remote_name(self): + """The name of the remote set to be the upstream of this branch.""" + return self._branch.remote_name + + @Property(str) + def upstream_name(self): + """The name of the reference set to be the upstream of this one""" + return self._branch.upstream_name + + # upstream + # The branch’s upstream branch or None if this branch does not have an upstream set. Set to None + # to unset the upstream configuration. + + ############################################## + + @Slot() + def delete(self): + """Delete this branch. It will no longer be valid!""" + self._branch.delete() # DANGER + + @Slot(str, bool) + def rename(self, name, force=False): + """Move/rename an existing local branch reference. The new branch name will be checked for validity. Returns the new branch.""" + self._branch.rename(name, force) + +#################################################################################################### + +class QmlReference(QmlRepositoryChild): + + # https://www.pygit2.org/references.html#the-reference-type + + ############################################## + + def __init__(self, repository, reference: pygit.Reference): + super().__init__(repository) + self._reference = reference + + ############################################## + + @Property(str, constant=True) + def name(self): + """The full name of the reference.""" + return self._reference.name + + @Property(str, constant=True) + def shorthand(self): + """The shorthand “human-readable” name of the reference.""" + return self._reference.name + + @Slot(bool) + def is_oid(self, name): + """""" + self._reference.type == pygit.GIT_REF_OID + + @Slot(bool) + def is_symbolic(self, name): + """""" + self._reference.type == pygit.GIT_REF_SYMBOLIC + + ############################################## + + @Slot() + def delete(self, name): + """Delete this reference. It will no longer be valid!""" + self._reference.delete() # DANGER + + @Slot(str) + def rename(self, name): + """Rename the reference.""" + self._reference.rename(name) + + # .set_target(target[, message]) + # Set the target of this reference. + + # @Slot(result=QmlReference) + def resolve(self): + """Resolve a symbolic reference and return a direct reference.""" + return self.__class__(self._repository, self._reference.resolve()) # Fixme: keep ref. ? + + # @Slot(result=QmlCommit) + def commit(self): + """""" + commit = self._reference.peel() + return self._repository.commit_pool.from_commit(commit) + +QmlReference.resolve = Slot(result=QmlReference)(QmlReference.resolve) + +#################################################################################################### + +class QmlCommit(QmlRepositoryChild): + + # https://www.pygit2.org/objects.html#commits + + ############################################## + + def __init__(self, repository, commit: pygit.Commit): + super().__init__(repository) + self._commit = commit + + ############################################## + + @Property(str, constant=True) + def message(self): + return self._commit.message + + ############################################## + + @Property(str, constant=True) + def sha(self): + return str(self._commit.hex) + + ############################################## + + @Property(str, constant=True) + def time(self): + return fromtimestamp(self._commit.commit_time).strftime('%Y-%m-%d %H:%M:%S'), + + ############################################## + + @Property(str, constant=True) + def committer(self): + committer = self._commit.committer + return '{} <{}>'.format(committer.name, committer.email), + + ############################################## + + @Property(str, constant=True) + def author(self): + author = self._commit.author + return '{} <{}>'.format(author.name, author.email), + +QmlReference.commit = Slot(result=QmlCommit)(QmlReference.commit) + +#################################################################################################### + +class QmlCommitPool(QmlRepositoryChild): + + ############################################## + + def __init__(self, repository): + super().__init__(repository) + self._commits = {} + + ############################################## + + def from_commit(self, commit): + sha = str(commit.hex) + if sha in self._commits: + return self._commits[sha] + else: + qml_commit = QmlCommit(self._repository, commit) + self._commits[sha] = qml_commit + return qml_commit + + ############################################## + + @Slot(str, result=QmlCommit) + def get(self, sha): + return self._commits.get(sha, None) + +#################################################################################################### + +class QmlNote(QmlRepositoryChild): + + # https://www.pygit2.org/references.html#notes + # GIT-NOTES(1) + + ############################################## + + def __init__(self, repository, note): + super().__init__(repository) + self._note = {} + + ############################################## + + @property + def message(self): + return self._message + +#################################################################################################### + +class QmlRepository(QObject): + + _logger = _module_logger.getChild('QmlRepository') + + ############################################## + + def __init__(self, path=None): + + super().__init__() + + self._logger.info('Init Repository') + + if path is None: + path = os.getcwd() + try: + self._repository = GitRepository(path) + except RepositoryNotFound: + raise NameError("Any Git repository was found in path {}".format(path)) + self._repository = None + return + + self._branches = [] + self._update_branches() + + self._commit_pool = QmlCommitPool(self) + self._commits = [] + self._update_commits() + + self._tags = [] + self._update_tags() + + ############################################## + + def __bool__(self): + return self._repository is not None + + ############################################## + + @Property(QmlCommitPool, constant=True) + def commit_pool(self): + return self._commit_pool + + ############################################## + + def _update_branches(self): + self._branches = [QmlBranch(self, branch) for branch in self._repository.branches] + self.branches_changed.emit() + + branches_changed = Signal() + + @Property(QQmlListProperty, notify=branches_changed) + def branches(self): + return QQmlListProperty(QmlBranch, self, self._branches) + + ############################################## + + def _update_commits(self): + self._commits = [self._commit_pool.from_commit(commit) for commit in self._repository.commits_for_head] + self.commits_changed.emit() + + commits_changed = Signal() + + @Property(QQmlListProperty, notify=commits_changed) + def commits(self): + return QQmlListProperty(QmlCommit, self, self._commits) + + ############################################## + + def _update_tags(self): + self._tags = [QmlReference(self, reference) for reference in self._repository.tags] + self.tags_changed.emit() + + tags_changed = Signal() + + @Property(QQmlListProperty, notify=tags_changed) + def tags(self): + return QQmlListProperty(QmlReference, self, self._tags) diff --git a/CodeReview/QmlApplication/qml/Widgets/BranchList.qml b/CodeReview/QmlApplication/qml/Widgets/BranchList.qml new file mode 100644 index 0000000..f520803 --- /dev/null +++ b/CodeReview/QmlApplication/qml/Widgets/BranchList.qml @@ -0,0 +1,44 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2020 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.11 + +ListView { + id: branch_listview + + model: null + + spacing: 2 + + clip: true + + delegate: Rectangle { + width: branch_listview.width + height: childrenRect.height + + RowLayout { + Label { + text: modelData.name + } + } + } +} diff --git a/CodeReview/QmlApplication/qml/Widgets/CommitLog.qml b/CodeReview/QmlApplication/qml/Widgets/CommitLog.qml new file mode 100644 index 0000000..bcd2d02 --- /dev/null +++ b/CodeReview/QmlApplication/qml/Widgets/CommitLog.qml @@ -0,0 +1,44 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2020 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.11 + +ListView { + id: commit_log_listview + + model: null + + spacing: 2 + + clip: true + + delegate: Rectangle { + width: commit_log_listview.width + height: childrenRect.height + + RowLayout { + Label { + text: modelData.message + } + } + } +} diff --git a/CodeReview/QmlApplication/qml/Widgets/TagList.qml b/CodeReview/QmlApplication/qml/Widgets/TagList.qml new file mode 100644 index 0000000..4f9d04e --- /dev/null +++ b/CodeReview/QmlApplication/qml/Widgets/TagList.qml @@ -0,0 +1,44 @@ +/*************************************************************************************************** + * + * CodeReview - A Code Review GUI + * Copyright (C) 2020 Fabrice Salvaire + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + ***************************************************************************************************/ + +import QtQuick 2.11 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.11 + +ListView { + id: tag_listview + + model: null + + spacing: 2 + + clip: true + + delegate: Rectangle { + width: tag_listview.width + height: childrenRect.height + + RowLayout { + Label { + text: modelData.name + } + } + } +} diff --git a/CodeReview/QmlApplication/qml/Widgets/qmldir b/CodeReview/QmlApplication/qml/Widgets/qmldir index 46adc9b..a24b512 100644 --- a/CodeReview/QmlApplication/qml/Widgets/qmldir +++ b/CodeReview/QmlApplication/qml/Widgets/qmldir @@ -1,6 +1,9 @@ module Widgets AboutDialog 1.0 AboutDialog.qml +BranchList 1.0 BranchList.qml CentredDialog 1.0 CentredDialog.qml +CommitLog 1.0 CommitLog.qml ErrorMessageDialog 1.0 ErrorMessageDialog.qml MarkdownViewer 1.0 MarkdownViewer.qml ShortcutRow 1.0 ShortcutRow.qml +TagList 1.0 TagList.qml diff --git a/CodeReview/QmlApplication/qml/main.qml b/CodeReview/QmlApplication/qml/main.qml index af93723..abc4287 100644 --- a/CodeReview/QmlApplication/qml/main.qml +++ b/CodeReview/QmlApplication/qml/main.qml @@ -70,6 +70,8 @@ ApplicationWindow { application.show_error.connect(on_error) application_window.showMaximized() + application.repository_changed.connect(on_repository_loaded) + // Fixme: prevent crash when opening option dialog // RuntimeError: wrapped C/C++ object of type Shortcut has been deleted let _shortcuts = application_settings.shortcuts @@ -92,6 +94,14 @@ ApplicationWindow { error_message_dialog.open_with_message(text) } + function on_repository_loaded() { + console.info('on_repository_loaded') + var repository = application.repository + branch_listview.model = repository.branches + tag_listview.model = repository.tags + commit_log.model = repository.commits + } + /******************************************************* * * Slots @@ -161,6 +171,49 @@ ApplicationWindow { * */ + // ColumnLayout { + Column { + id: central_widget + anchors.fill: parent + spacing: 2 + + Item { + // Layout.fillHeight: true + height: parent.height / 4 + width: parent.width + + Widgets.BranchList { + id: branch_listview + enabled: application.repository + anchors.fill: parent + } + } + + Item { + // Layout.fillHeight: true + height: parent.height / 4 + width: parent.width + + Widgets.TagList { + id: tag_listview + enabled: application.repository + anchors.fill: parent + } + } + + Item { + // Layout.fillHeight: true + height: parent.height / 2 + width: parent.width + + Widgets.CommitLog { + id: commit_log + enabled: application.repository + anchors.fill: parent + } + } + } + /******************************************************* * * Footer diff --git a/CodeReview/Repository/Git.py b/CodeReview/Repository/Git.py index 1c4ef1c..18648f7 100644 --- a/CodeReview/Repository/Git.py +++ b/CodeReview/Repository/Git.py @@ -16,13 +16,15 @@ # #################################################################################################### +__all__ = ['GitRepository', 'RepositoryNotFound', 'Diff', 'pygit'] + #################################################################################################### from pathlib import Path import logging import re -import pygit2 as git +import pygit2 as pygit #################################################################################################### @@ -35,8 +37,41 @@ class RepositoryNotFound(Exception): #################################################################################################### +class Diff: + + ############################################## + + def __init__(self, diff, patches): + self.diff = diff + self._patches = patches + + ############################################## + + def __len__(self): + return len(self._patches) + + ############################################## + + def __iter__(self): + return iter(self._patches) + + ############################################## + + def __getitem__(self, i): + return self._patches[i] + + ############################################## + + def new_paths(self): + return [patch.delta.new_file.path for patch in self._patches] + +#################################################################################################### + class GitRepository: + """Class to implement a wrapper on top pygit2. + """ + _logger = _module_logger.getChild('GitRepository') INDEX_PATH = Path('.git').joinpath('index') @@ -49,8 +84,8 @@ def __init__(self, path): path = Path(path).absolute().resolve() try: - repository_path = git.discover_repository(str(path)) - self._repository = git.Repository(repository_path) + repository_path = pygit.discover_repository(str(path)) + self._repository = pygit.Repository(repository_path) self._workdir = Path(self._repository.workdir) self._path_filter = str(path.relative_to(self._workdir)) if self._path_filter == '.': @@ -79,7 +114,7 @@ def repository_status(self): return self._repository.status() @property - def branch_name(self): + def branch_name(self) -> str: # head = self._repository.lookup_reference('HEAD').resolve() head = self._repository.head return head.name @@ -91,37 +126,73 @@ def join_repository_path(self, path): ############################################## + def references(self, regexp=None) -> pygit.Reference: + # https://www.pygit2.org/references.html + if regexp is not None: + regexp_ = re.compile(str(regexp)) + return [ + self._repository.references[name] + for name in self._repository.references if regexp_.match(name) + ] + else: + return [self._repository.references[name] for name in self._repository.references] + + ############################################## + @property - def tags(self): - regex = re.compile('^refs/tags') - return [ - self._repository.references[name] - for name in self._repository.references if regex.match(name) - ] + def tags(self) -> pygit.Reference: + return self.references('^refs/tags') ############################################## @property - def commits(self): + def branches(self) -> pygit.Branch: + # https://www.pygit2.org/branches.html + return [self._repository.branches[name] for name in self._repository.branches] + + ############################################## + + def commits(self, oid, topological=True, by_time=False): # -> iter on pygit.Commit + + # https://www.pygit2.org/commit_log.html + # https://www.pygit2.org/commit_log.html?highlight=walk#pygit2.Repository.walk - head = self._repository.head - head_commit = self._repository[head.target] # GIT_SORT_TOPOLOGICAL. Sort the repository contents in topological order (parents before children); # this sorting mode can be combined with time sorting. - sorting = git.GIT_SORT_TOPOLOGICAL # git.GIT_SORT_TIME - commits = [commit for commit in self._repository.walk(head_commit.id, sorting)] + if topological and not by_time: + sorting = pygit.GIT_SORT_TOPOLOGICAL + if topological and by_time: + sorting = pygit.GIT_SORT_TOPOLOGICAL | pygit.GIT_SORT_TIME + else: + sorting = pygit.GIT_SORT_NONE + + commits = self._repository.walk(oid, sorting) # -> pygit.Walker return commits ############################################## - def diff(self, a=None, b=None, cached=False, path_filter=None): + @property + def commits_for_head(self): # -> iter on pygit.Commit + + # https://www.pygit2.org/references.html#the-head + # Current head reference of the repository + # head = repo.references['HEAD'].resolve() + head = self._repository.head # -> pygit.Reference + head_commit = self._repository[head.target] # -> pygit.Commit + oid = head_commit.id + + return self.commits(oid) + + ############################################## + + def diff(self, a=None, b=None, cached=False, path_filter=None) -> Diff: - if isinstance(a, git.Commit): + if isinstance(a, pygit.Commit): a_str = a.hex else: a_str = str(a) - if isinstance(b, git.Commit): + if isinstance(b, pygit.Commit): b_str = b.hex else: b_str = str(b) @@ -147,7 +218,7 @@ def diff(self, a=None, b=None, cached=False, path_filter=None): ############################################## - def file_content(self, oid): + def file_content(self, oid) -> str: try: return self._repository[oid].data.decode('utf-8') except KeyError: @@ -156,15 +227,15 @@ def file_content(self, oid): ############################################## _STATUS_TEXT = { - git.GIT_STATUS_CONFLICTED: 'conflicted', - git.GIT_STATUS_CURRENT: 'current', - git.GIT_STATUS_IGNORED: 'ignored', - git.GIT_STATUS_INDEX_DELETED: 'index deleted', - git.GIT_STATUS_INDEX_MODIFIED: 'index modified', - git.GIT_STATUS_INDEX_NEW: 'index new', - git.GIT_STATUS_WT_DELETED: 'working tree deleted', - git.GIT_STATUS_WT_MODIFIED: 'working tree modified', - git.GIT_STATUS_WT_NEW: 'working tree new', + pygit.GIT_STATUS_CONFLICTED: 'conflicted', + pygit.GIT_STATUS_CURRENT: 'current', + pygit.GIT_STATUS_IGNORED: 'ignored', + pygit.GIT_STATUS_INDEX_DELETED: 'index deleted', + pygit.GIT_STATUS_INDEX_MODIFIED: 'index modified', + pygit.GIT_STATUS_INDEX_NEW: 'index new', + pygit.GIT_STATUS_WT_DELETED: 'working tree deleted', + pygit.GIT_STATUS_WT_MODIFIED: 'working tree modified', + pygit.GIT_STATUS_WT_NEW: 'working tree new', } def status(self, path): @@ -181,7 +252,7 @@ def status(self, path): ############################################## - def is_staged(self, path): + def is_staged(self, path) -> bool: # index = self._repository.index # head_tree = self._repository.revparse_single('HEAD').tree @@ -191,12 +262,12 @@ def is_staged(self, path): # return index_oid != head_oid # except KeyError: # return False # untracked file - return self.status(path) == git.GIT_STATUS_INDEX_MODIFIED + return self.status(path) == pygit.GIT_STATUS_INDEX_MODIFIED ############################################## - def is_modified(self, path): - return self.status(path) == git.GIT_STATUS_WT_MODIFIED + def is_modified(self, path) -> bool: + return self.status(path) == pygit.GIT_STATUS_WT_MODIFIED ############################################## @@ -216,37 +287,7 @@ def unstage(self, path): if path in head_tree: # Restore index to HEAD tree_entry = head_tree[path] - index_entry = git.IndexEntry(path, tree_entry.oid, tree_entry.filemode) + index_entry = pygit.IndexEntry(path, tree_entry.oid, tree_entry.filemode) self._logger.info("Reset index to HEAD for {}".format(path)) index.add(index_entry) index.write() - -#################################################################################################### - -class Diff: - - ############################################## - - def __init__(self, diff, patches): - self.diff = diff - self._patches = patches - - ############################################## - - def __len__(self): - return len(self._patches) - - ############################################## - - def __iter__(self): - return iter(self._patches) - - ############################################## - - def __getitem__(self, i): - return self._patches[i] - - ############################################## - - def new_paths(self): - return [patch.delta.new_file.path for patch in self._patches] diff --git a/bin/pyqgit-qml b/bin/pyqgit-qml index bb317ea..cd0b039 100755 --- a/bin/pyqgit-qml +++ b/bin/pyqgit-qml @@ -27,7 +27,7 @@ logger = Logging.setup_logging('pyqgit') #################################################################################################### -from CodeReview.QmlApplication.QmlApplication import Application +from CodeReview.QmlApplication.QmlMainApplication import Application #################################################################################################### From c7192da0490a4a3be34ae74619d96a339e3b6bd3 Mon Sep 17 00:00:00 2001 From: Fabrice Salvaire Date: Thu, 14 May 2020 01:12:31 +0200 Subject: [PATCH 30/69] removed gitk tcl source --- ressources/gitk | 12619 ---------------------------------------------- 1 file changed, 12619 deletions(-) delete mode 100755 ressources/gitk diff --git a/ressources/gitk b/ressources/gitk deleted file mode 100755 index a14d7a1..0000000 --- a/ressources/gitk +++ /dev/null @@ -1,12619 +0,0 @@ -#!/bin/sh -# Tcl ignores the next line -*- tcl -*- \ -exec wish "$0" -- "$@" - -# Copyright © 2005-2016 Paul Mackerras. All rights reserved. -# This program is free software; it may be used, copied, modified -# and distributed under the terms of the GNU General Public Licence, -# either version 2, or (at your option) any later version. - -package require Tk - -proc hasworktree {} { - return [expr {[exec git rev-parse --is-bare-repository] == "false" && - [exec git rev-parse --is-inside-git-dir] == "false"}] -} - -proc reponame {} { - global gitdir - set n [file normalize $gitdir] - if {[string match "*/.git" $n]} { - set n [string range $n 0 end-5] - } - return [file tail $n] -} - -proc gitworktree {} { - variable _gitworktree - if {[info exists _gitworktree]} { - return $_gitworktree - } - # v1.7.0 introduced --show-toplevel to return the canonical work-tree - if {[catch {set _gitworktree [exec git rev-parse --show-toplevel]}]} { - # try to set work tree from environment, core.worktree or use - # cdup to obtain a relative path to the top of the worktree. If - # run from the top, the ./ prefix ensures normalize expands pwd. - if {[catch { set _gitworktree $env(GIT_WORK_TREE) }]} { - catch {set _gitworktree [exec git config --get core.worktree]} - if {$_gitworktree eq ""} { - set _gitworktree [file normalize ./[exec git rev-parse --show-cdup]] - } - } - } - return $_gitworktree -} - -# A simple scheduler for compute-intensive stuff. -# The aim is to make sure that event handlers for GUI actions can -# run at least every 50-100 ms. Unfortunately fileevent handlers are -# run before X event handlers, so reading from a fast source can -# make the GUI completely unresponsive. -proc run args { - global isonrunq runq currunq - - set script $args - if {[info exists isonrunq($script)]} return - if {$runq eq {} && ![info exists currunq]} { - after idle dorunq - } - lappend runq [list {} $script] - set isonrunq($script) 1 -} - -proc filerun {fd script} { - fileevent $fd readable [list filereadable $fd $script] -} - -proc filereadable {fd script} { - global runq currunq - - fileevent $fd readable {} - if {$runq eq {} && ![info exists currunq]} { - after idle dorunq - } - lappend runq [list $fd $script] -} - -proc nukefile {fd} { - global runq - - for {set i 0} {$i < [llength $runq]} {} { - if {[lindex $runq $i 0] eq $fd} { - set runq [lreplace $runq $i $i] - } else { - incr i - } - } -} - -proc dorunq {} { - global isonrunq runq currunq - - set tstart [clock clicks -milliseconds] - set t0 $tstart - while {[llength $runq] > 0} { - set fd [lindex $runq 0 0] - set script [lindex $runq 0 1] - set currunq [lindex $runq 0] - set runq [lrange $runq 1 end] - set repeat [eval $script] - unset currunq - set t1 [clock clicks -milliseconds] - set t [expr {$t1 - $t0}] - if {$repeat ne {} && $repeat} { - if {$fd eq {} || $repeat == 2} { - # script returns 1 if it wants to be readded - # file readers return 2 if they could do more straight away - lappend runq [list $fd $script] - } else { - fileevent $fd readable [list filereadable $fd $script] - } - } elseif {$fd eq {}} { - unset isonrunq($script) - } - set t0 $t1 - if {$t1 - $tstart >= 80} break - } - if {$runq ne {}} { - after idle dorunq - } -} - -proc reg_instance {fd} { - global commfd leftover loginstance - - set i [incr loginstance] - set commfd($i) $fd - set leftover($i) {} - return $i -} - -proc unmerged_files {files} { - global nr_unmerged - - # find the list of unmerged files - set mlist {} - set nr_unmerged 0 - if {[catch { - set fd [open "| git ls-files -u" r] - } err]} { - show_error {} . "[mc "Couldn't get list of unmerged files:"] $err" - exit 1 - } - while {[gets $fd line] >= 0} { - set i [string first "\t" $line] - if {$i < 0} continue - set fname [string range $line [expr {$i+1}] end] - if {[lsearch -exact $mlist $fname] >= 0} continue - incr nr_unmerged - if {$files eq {} || [path_filter $files $fname]} { - lappend mlist $fname - } - } - catch {close $fd} - return $mlist -} - -proc parseviewargs {n arglist} { - global vdatemode vmergeonly vflags vdflags vrevs vfiltered vorigargs env - global vinlinediff - global worddiff git_version - - set vdatemode($n) 0 - set vmergeonly($n) 0 - set vinlinediff($n) 0 - set glflags {} - set diffargs {} - set nextisval 0 - set revargs {} - set origargs $arglist - set allknown 1 - set filtered 0 - set i -1 - foreach arg $arglist { - incr i - if {$nextisval} { - lappend glflags $arg - set nextisval 0 - continue - } - switch -glob -- $arg { - "-d" - - "--date-order" { - set vdatemode($n) 1 - # remove from origargs in case we hit an unknown option - set origargs [lreplace $origargs $i $i] - incr i -1 - } - "-[puabwcrRBMC]" - - "--no-renames" - "--full-index" - "--binary" - "--abbrev=*" - - "--find-copies-harder" - "-l*" - "--ext-diff" - "--no-ext-diff" - - "--src-prefix=*" - "--dst-prefix=*" - "--no-prefix" - - "-O*" - "--text" - "--full-diff" - "--ignore-space-at-eol" - - "--ignore-space-change" - "-U*" - "--unified=*" { - # These request or affect diff output, which we don't want. - # Some could be used to set our defaults for diff display. - lappend diffargs $arg - } - "--raw" - "--patch-with-raw" - "--patch-with-stat" - - "--name-only" - "--name-status" - "--color" - - "--log-size" - "--pretty=*" - "--decorate" - "--abbrev-commit" - - "--cc" - "-z" - "--header" - "--parents" - "--boundary" - - "--no-color" - "-g" - "--walk-reflogs" - "--no-walk" - - "--timestamp" - "relative-date" - "--date=*" - "--stdin" - - "--objects" - "--objects-edge" - "--reverse" { - # These cause our parsing of git log's output to fail, or else - # they're options we want to set ourselves, so ignore them. - } - "--color-words*" - "--word-diff=color" { - # These trigger a word diff in the console interface, - # so help the user by enabling our own support - if {[package vcompare $git_version "1.7.2"] >= 0} { - set worddiff [mc "Color words"] - } - } - "--word-diff*" { - if {[package vcompare $git_version "1.7.2"] >= 0} { - set worddiff [mc "Markup words"] - } - } - "--stat=*" - "--numstat" - "--shortstat" - "--summary" - - "--check" - "--exit-code" - "--quiet" - "--topo-order" - - "--full-history" - "--dense" - "--sparse" - - "--follow" - "--left-right" - "--encoding=*" { - # These are harmless, and some are even useful - lappend glflags $arg - } - "--diff-filter=*" - "--no-merges" - "--unpacked" - - "--max-count=*" - "--skip=*" - "--since=*" - "--after=*" - - "--until=*" - "--before=*" - "--max-age=*" - "--min-age=*" - - "--author=*" - "--committer=*" - "--grep=*" - "-[iE]" - - "--remove-empty" - "--first-parent" - "--cherry-pick" - - "-S*" - "-G*" - "--pickaxe-all" - "--pickaxe-regex" - - "--simplify-by-decoration" { - # These mean that we get a subset of the commits - set filtered 1 - lappend glflags $arg - } - "-L*" { - # Line-log with 'stuck' argument (unstuck form is - # not supported) - set filtered 1 - set vinlinediff($n) 1 - set allknown 0 - lappend glflags $arg - } - "-n" { - # This appears to be the only one that has a value as a - # separate word following it - set filtered 1 - set nextisval 1 - lappend glflags $arg - } - "--not" - "--all" { - lappend revargs $arg - } - "--merge" { - set vmergeonly($n) 1 - # git rev-parse doesn't understand --merge - lappend revargs --gitk-symmetric-diff-marker MERGE_HEAD...HEAD - } - "--no-replace-objects" { - set env(GIT_NO_REPLACE_OBJECTS) "1" - } - "-*" { - # Other flag arguments including - - if {[string is digit -strict [string range $arg 1 end]]} { - set filtered 1 - } else { - # a flag argument that we don't recognize; - # that means we can't optimize - set allknown 0 - } - lappend glflags $arg - } - default { - # Non-flag arguments specify commits or ranges of commits - if {[string match "*...*" $arg]} { - lappend revargs --gitk-symmetric-diff-marker - } - lappend revargs $arg - } - } - } - set vdflags($n) $diffargs - set vflags($n) $glflags - set vrevs($n) $revargs - set vfiltered($n) $filtered - set vorigargs($n) $origargs - return $allknown -} - -proc parseviewrevs {view revs} { - global vposids vnegids - - if {$revs eq {}} { - set revs HEAD - } elseif {[lsearch -exact $revs --all] >= 0} { - lappend revs HEAD - } - if {[catch {set ids [eval exec git rev-parse $revs]} err]} { - # we get stdout followed by stderr in $err - # for an unknown rev, git rev-parse echoes it and then errors out - set errlines [split $err "\n"] - set badrev {} - for {set l 0} {$l < [llength $errlines]} {incr l} { - set line [lindex $errlines $l] - if {!([string length $line] == 40 && [string is xdigit $line])} { - if {[string match "fatal:*" $line]} { - if {[string match "fatal: ambiguous argument*" $line] - && $badrev ne {}} { - if {[llength $badrev] == 1} { - set err "unknown revision $badrev" - } else { - set err "unknown revisions: [join $badrev ", "]" - } - } else { - set err [join [lrange $errlines $l end] "\n"] - } - break - } - lappend badrev $line - } - } - error_popup "[mc "Error parsing revisions:"] $err" - return {} - } - set ret {} - set pos {} - set neg {} - set sdm 0 - foreach id [split $ids "\n"] { - if {$id eq "--gitk-symmetric-diff-marker"} { - set sdm 4 - } elseif {[string match "^*" $id]} { - if {$sdm != 1} { - lappend ret $id - if {$sdm == 3} { - set sdm 0 - } - } - lappend neg [string range $id 1 end] - } else { - if {$sdm != 2} { - lappend ret $id - } else { - lset ret end $id...[lindex $ret end] - } - lappend pos $id - } - incr sdm -1 - } - set vposids($view) $pos - set vnegids($view) $neg - return $ret -} - -# Start off a git log process and arrange to read its output -proc start_rev_list {view} { - global startmsecs commitidx viewcomplete curview - global tclencoding - global viewargs viewargscmd viewfiles vfilelimit - global showlocalchanges - global viewactive viewinstances vmergeonly - global mainheadid viewmainheadid viewmainheadid_orig - global vcanopt vflags vrevs vorigargs - global show_notes - - set startmsecs [clock clicks -milliseconds] - set commitidx($view) 0 - # these are set this way for the error exits - set viewcomplete($view) 1 - set viewactive($view) 0 - varcinit $view - - set args $viewargs($view) - if {$viewargscmd($view) ne {}} { - if {[catch { - set str [exec sh -c $viewargscmd($view)] - } err]} { - error_popup "[mc "Error executing --argscmd command:"] $err" - return 0 - } - set args [concat $args [split $str "\n"]] - } - set vcanopt($view) [parseviewargs $view $args] - - set files $viewfiles($view) - if {$vmergeonly($view)} { - set files [unmerged_files $files] - if {$files eq {}} { - global nr_unmerged - if {$nr_unmerged == 0} { - error_popup [mc "No files selected: --merge specified but\ - no files are unmerged."] - } else { - error_popup [mc "No files selected: --merge specified but\ - no unmerged files are within file limit."] - } - return 0 - } - } - set vfilelimit($view) $files - - if {$vcanopt($view)} { - set revs [parseviewrevs $view $vrevs($view)] - if {$revs eq {}} { - return 0 - } - set args [concat $vflags($view) $revs] - } else { - set args $vorigargs($view) - } - - if {[catch { - set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ - --parents --boundary $args "--" $files] r] - } err]} { - error_popup "[mc "Error executing git log:"] $err" - return 0 - } - set i [reg_instance $fd] - set viewinstances($view) [list $i] - set viewmainheadid($view) $mainheadid - set viewmainheadid_orig($view) $mainheadid - if {$files ne {} && $mainheadid ne {}} { - get_viewmainhead $view - } - if {$showlocalchanges && $viewmainheadid($view) ne {}} { - interestedin $viewmainheadid($view) dodiffindex - } - fconfigure $fd -blocking 0 -translation lf -eofchar {} - if {$tclencoding != {}} { - fconfigure $fd -encoding $tclencoding - } - filerun $fd [list getcommitlines $fd $i $view 0] - nowbusy $view [mc "Reading"] - set viewcomplete($view) 0 - set viewactive($view) 1 - return 1 -} - -proc stop_instance {inst} { - global commfd leftover - - set fd $commfd($inst) - catch { - set pid [pid $fd] - - if {$::tcl_platform(platform) eq {windows}} { - exec taskkill /pid $pid - } else { - exec kill $pid - } - } - catch {close $fd} - nukefile $fd - unset commfd($inst) - unset leftover($inst) -} - -proc stop_backends {} { - global commfd - - foreach inst [array names commfd] { - stop_instance $inst - } -} - -proc stop_rev_list {view} { - global viewinstances - - foreach inst $viewinstances($view) { - stop_instance $inst - } - set viewinstances($view) {} -} - -proc reset_pending_select {selid} { - global pending_select mainheadid selectheadid - - if {$selid ne {}} { - set pending_select $selid - } elseif {$selectheadid ne {}} { - set pending_select $selectheadid - } else { - set pending_select $mainheadid - } -} - -proc getcommits {selid} { - global canv curview need_redisplay viewactive - - initlayout - if {[start_rev_list $curview]} { - reset_pending_select $selid - show_status [mc "Reading commits..."] - set need_redisplay 1 - } else { - show_status [mc "No commits selected"] - } -} - -proc updatecommits {} { - global curview vcanopt vorigargs vfilelimit viewinstances - global viewactive viewcomplete tclencoding - global startmsecs showneartags showlocalchanges - global mainheadid viewmainheadid viewmainheadid_orig pending_select - global hasworktree - global varcid vposids vnegids vflags vrevs - global show_notes - - set hasworktree [hasworktree] - rereadrefs - set view $curview - if {$mainheadid ne $viewmainheadid_orig($view)} { - if {$showlocalchanges} { - dohidelocalchanges - } - set viewmainheadid($view) $mainheadid - set viewmainheadid_orig($view) $mainheadid - if {$vfilelimit($view) ne {}} { - get_viewmainhead $view - } - } - if {$showlocalchanges} { - doshowlocalchanges - } - if {$vcanopt($view)} { - set oldpos $vposids($view) - set oldneg $vnegids($view) - set revs [parseviewrevs $view $vrevs($view)] - if {$revs eq {}} { - return - } - # note: getting the delta when negative refs change is hard, - # and could require multiple git log invocations, so in that - # case we ask git log for all the commits (not just the delta) - if {$oldneg eq $vnegids($view)} { - set newrevs {} - set npos 0 - # take out positive refs that we asked for before or - # that we have already seen - foreach rev $revs { - if {[string length $rev] == 40} { - if {[lsearch -exact $oldpos $rev] < 0 - && ![info exists varcid($view,$rev)]} { - lappend newrevs $rev - incr npos - } - } else { - lappend $newrevs $rev - } - } - if {$npos == 0} return - set revs $newrevs - set vposids($view) [lsort -unique [concat $oldpos $vposids($view)]] - } - set args [concat $vflags($view) $revs --not $oldpos] - } else { - set args $vorigargs($view) - } - if {[catch { - set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ - --parents --boundary $args "--" $vfilelimit($view)] r] - } err]} { - error_popup "[mc "Error executing git log:"] $err" - return - } - if {$viewactive($view) == 0} { - set startmsecs [clock clicks -milliseconds] - } - set i [reg_instance $fd] - lappend viewinstances($view) $i - fconfigure $fd -blocking 0 -translation lf -eofchar {} - if {$tclencoding != {}} { - fconfigure $fd -encoding $tclencoding - } - filerun $fd [list getcommitlines $fd $i $view 1] - incr viewactive($view) - set viewcomplete($view) 0 - reset_pending_select {} - nowbusy $view [mc "Reading"] - if {$showneartags} { - getallcommits - } -} - -proc reloadcommits {} { - global curview viewcomplete selectedline currentid thickerline - global showneartags treediffs commitinterest cached_commitrow - global targetid commitinfo - - set selid {} - if {$selectedline ne {}} { - set selid $currentid - } - - if {!$viewcomplete($curview)} { - stop_rev_list $curview - } - resetvarcs $curview - set selectedline {} - unset -nocomplain currentid - unset -nocomplain thickerline - unset -nocomplain treediffs - readrefs - changedrefs - if {$showneartags} { - getallcommits - } - clear_display - unset -nocomplain commitinfo - unset -nocomplain commitinterest - unset -nocomplain cached_commitrow - unset -nocomplain targetid - setcanvscroll - getcommits $selid - return 0 -} - -# This makes a string representation of a positive integer which -# sorts as a string in numerical order -proc strrep {n} { - if {$n < 16} { - return [format "%x" $n] - } elseif {$n < 256} { - return [format "x%.2x" $n] - } elseif {$n < 65536} { - return [format "y%.4x" $n] - } - return [format "z%.8x" $n] -} - -# Procedures used in reordering commits from git log (without -# --topo-order) into the order for display. - -proc varcinit {view} { - global varcstart vupptr vdownptr vleftptr vbackptr varctok varcrow - global vtokmod varcmod vrowmod varcix vlastins - - set varcstart($view) {{}} - set vupptr($view) {0} - set vdownptr($view) {0} - set vleftptr($view) {0} - set vbackptr($view) {0} - set varctok($view) {{}} - set varcrow($view) {{}} - set vtokmod($view) {} - set varcmod($view) 0 - set vrowmod($view) 0 - set varcix($view) {{}} - set vlastins($view) {0} -} - -proc resetvarcs {view} { - global varcid varccommits parents children vseedcount ordertok - global vshortids - - foreach vid [array names varcid $view,*] { - unset varcid($vid) - unset children($vid) - unset parents($vid) - } - foreach vid [array names vshortids $view,*] { - unset vshortids($vid) - } - # some commits might have children but haven't been seen yet - foreach vid [array names children $view,*] { - unset children($vid) - } - foreach va [array names varccommits $view,*] { - unset varccommits($va) - } - foreach vd [array names vseedcount $view,*] { - unset vseedcount($vd) - } - unset -nocomplain ordertok -} - -# returns a list of the commits with no children -proc seeds {v} { - global vdownptr vleftptr varcstart - - set ret {} - set a [lindex $vdownptr($v) 0] - while {$a != 0} { - lappend ret [lindex $varcstart($v) $a] - set a [lindex $vleftptr($v) $a] - } - return $ret -} - -proc newvarc {view id} { - global varcid varctok parents children vdatemode - global vupptr vdownptr vleftptr vbackptr varcrow varcix varcstart - global commitdata commitinfo vseedcount varccommits vlastins - - set a [llength $varctok($view)] - set vid $view,$id - if {[llength $children($vid)] == 0 || $vdatemode($view)} { - if {![info exists commitinfo($id)]} { - parsecommit $id $commitdata($id) 1 - } - set cdate [lindex [lindex $commitinfo($id) 4] 0] - if {![string is integer -strict $cdate]} { - set cdate 0 - } - if {![info exists vseedcount($view,$cdate)]} { - set vseedcount($view,$cdate) -1 - } - set c [incr vseedcount($view,$cdate)] - set cdate [expr {$cdate ^ 0xffffffff}] - set tok "s[strrep $cdate][strrep $c]" - } else { - set tok {} - } - set ka 0 - if {[llength $children($vid)] > 0} { - set kid [lindex $children($vid) end] - set k $varcid($view,$kid) - if {[string compare [lindex $varctok($view) $k] $tok] > 0} { - set ki $kid - set ka $k - set tok [lindex $varctok($view) $k] - } - } - if {$ka != 0} { - set i [lsearch -exact $parents($view,$ki) $id] - set j [expr {[llength $parents($view,$ki)] - 1 - $i}] - append tok [strrep $j] - } - set c [lindex $vlastins($view) $ka] - if {$c == 0 || [string compare $tok [lindex $varctok($view) $c]] < 0} { - set c $ka - set b [lindex $vdownptr($view) $ka] - } else { - set b [lindex $vleftptr($view) $c] - } - while {$b != 0 && [string compare $tok [lindex $varctok($view) $b]] >= 0} { - set c $b - set b [lindex $vleftptr($view) $c] - } - if {$c == $ka} { - lset vdownptr($view) $ka $a - lappend vbackptr($view) 0 - } else { - lset vleftptr($view) $c $a - lappend vbackptr($view) $c - } - lset vlastins($view) $ka $a - lappend vupptr($view) $ka - lappend vleftptr($view) $b - if {$b != 0} { - lset vbackptr($view) $b $a - } - lappend varctok($view) $tok - lappend varcstart($view) $id - lappend vdownptr($view) 0 - lappend varcrow($view) {} - lappend varcix($view) {} - set varccommits($view,$a) {} - lappend vlastins($view) 0 - return $a -} - -proc splitvarc {p v} { - global varcid varcstart varccommits varctok vtokmod - global vupptr vdownptr vleftptr vbackptr varcix varcrow vlastins - - set oa $varcid($v,$p) - set otok [lindex $varctok($v) $oa] - set ac $varccommits($v,$oa) - set i [lsearch -exact $varccommits($v,$oa) $p] - if {$i <= 0} return - set na [llength $varctok($v)] - # "%" sorts before "0"... - set tok "$otok%[strrep $i]" - lappend varctok($v) $tok - lappend varcrow($v) {} - lappend varcix($v) {} - set varccommits($v,$oa) [lrange $ac 0 [expr {$i - 1}]] - set varccommits($v,$na) [lrange $ac $i end] - lappend varcstart($v) $p - foreach id $varccommits($v,$na) { - set varcid($v,$id) $na - } - lappend vdownptr($v) [lindex $vdownptr($v) $oa] - lappend vlastins($v) [lindex $vlastins($v) $oa] - lset vdownptr($v) $oa $na - lset vlastins($v) $oa 0 - lappend vupptr($v) $oa - lappend vleftptr($v) 0 - lappend vbackptr($v) 0 - for {set b [lindex $vdownptr($v) $na]} {$b != 0} {set b [lindex $vleftptr($v) $b]} { - lset vupptr($v) $b $na - } - if {[string compare $otok $vtokmod($v)] <= 0} { - modify_arc $v $oa - } -} - -proc renumbervarc {a v} { - global parents children varctok varcstart varccommits - global vupptr vdownptr vleftptr vbackptr vlastins varcid vtokmod vdatemode - - set t1 [clock clicks -milliseconds] - set todo {} - set isrelated($a) 1 - set kidchanged($a) 1 - set ntot 0 - while {$a != 0} { - if {[info exists isrelated($a)]} { - lappend todo $a - set id [lindex $varccommits($v,$a) end] - foreach p $parents($v,$id) { - if {[info exists varcid($v,$p)]} { - set isrelated($varcid($v,$p)) 1 - } - } - } - incr ntot - set b [lindex $vdownptr($v) $a] - if {$b == 0} { - while {$a != 0} { - set b [lindex $vleftptr($v) $a] - if {$b != 0} break - set a [lindex $vupptr($v) $a] - } - } - set a $b - } - foreach a $todo { - if {![info exists kidchanged($a)]} continue - set id [lindex $varcstart($v) $a] - if {[llength $children($v,$id)] > 1} { - set children($v,$id) [lsort -command [list vtokcmp $v] \ - $children($v,$id)] - } - set oldtok [lindex $varctok($v) $a] - if {!$vdatemode($v)} { - set tok {} - } else { - set tok $oldtok - } - set ka 0 - set kid [last_real_child $v,$id] - if {$kid ne {}} { - set k $varcid($v,$kid) - if {[string compare [lindex $varctok($v) $k] $tok] > 0} { - set ki $kid - set ka $k - set tok [lindex $varctok($v) $k] - } - } - if {$ka != 0} { - set i [lsearch -exact $parents($v,$ki) $id] - set j [expr {[llength $parents($v,$ki)] - 1 - $i}] - append tok [strrep $j] - } - if {$tok eq $oldtok} { - continue - } - set id [lindex $varccommits($v,$a) end] - foreach p $parents($v,$id) { - if {[info exists varcid($v,$p)]} { - set kidchanged($varcid($v,$p)) 1 - } else { - set sortkids($p) 1 - } - } - lset varctok($v) $a $tok - set b [lindex $vupptr($v) $a] - if {$b != $ka} { - if {[string compare [lindex $varctok($v) $ka] $vtokmod($v)] < 0} { - modify_arc $v $ka - } - if {[string compare [lindex $varctok($v) $b] $vtokmod($v)] < 0} { - modify_arc $v $b - } - set c [lindex $vbackptr($v) $a] - set d [lindex $vleftptr($v) $a] - if {$c == 0} { - lset vdownptr($v) $b $d - } else { - lset vleftptr($v) $c $d - } - if {$d != 0} { - lset vbackptr($v) $d $c - } - if {[lindex $vlastins($v) $b] == $a} { - lset vlastins($v) $b $c - } - lset vupptr($v) $a $ka - set c [lindex $vlastins($v) $ka] - if {$c == 0 || \ - [string compare $tok [lindex $varctok($v) $c]] < 0} { - set c $ka - set b [lindex $vdownptr($v) $ka] - } else { - set b [lindex $vleftptr($v) $c] - } - while {$b != 0 && \ - [string compare $tok [lindex $varctok($v) $b]] >= 0} { - set c $b - set b [lindex $vleftptr($v) $c] - } - if {$c == $ka} { - lset vdownptr($v) $ka $a - lset vbackptr($v) $a 0 - } else { - lset vleftptr($v) $c $a - lset vbackptr($v) $a $c - } - lset vleftptr($v) $a $b - if {$b != 0} { - lset vbackptr($v) $b $a - } - lset vlastins($v) $ka $a - } - } - foreach id [array names sortkids] { - if {[llength $children($v,$id)] > 1} { - set children($v,$id) [lsort -command [list vtokcmp $v] \ - $children($v,$id)] - } - } - set t2 [clock clicks -milliseconds] - #puts "renumbervarc did [llength $todo] of $ntot arcs in [expr {$t2-$t1}]ms" -} - -# Fix up the graph after we have found out that in view $v, -# $p (a commit that we have already seen) is actually the parent -# of the last commit in arc $a. -proc fix_reversal {p a v} { - global varcid varcstart varctok vupptr - - set pa $varcid($v,$p) - if {$p ne [lindex $varcstart($v) $pa]} { - splitvarc $p $v - set pa $varcid($v,$p) - } - # seeds always need to be renumbered - if {[lindex $vupptr($v) $pa] == 0 || - [string compare [lindex $varctok($v) $a] \ - [lindex $varctok($v) $pa]] > 0} { - renumbervarc $pa $v - } -} - -proc insertrow {id p v} { - global cmitlisted children parents varcid varctok vtokmod - global varccommits ordertok commitidx numcommits curview - global targetid targetrow vshortids - - readcommit $id - set vid $v,$id - set cmitlisted($vid) 1 - set children($vid) {} - set parents($vid) [list $p] - set a [newvarc $v $id] - set varcid($vid) $a - lappend vshortids($v,[string range $id 0 3]) $id - if {[string compare [lindex $varctok($v) $a] $vtokmod($v)] < 0} { - modify_arc $v $a - } - lappend varccommits($v,$a) $id - set vp $v,$p - if {[llength [lappend children($vp) $id]] > 1} { - set children($vp) [lsort -command [list vtokcmp $v] $children($vp)] - unset -nocomplain ordertok - } - fix_reversal $p $a $v - incr commitidx($v) - if {$v == $curview} { - set numcommits $commitidx($v) - setcanvscroll - if {[info exists targetid]} { - if {![comes_before $targetid $p]} { - incr targetrow - } - } - } -} - -proc insertfakerow {id p} { - global varcid varccommits parents children cmitlisted - global commitidx varctok vtokmod targetid targetrow curview numcommits - - set v $curview - set a $varcid($v,$p) - set i [lsearch -exact $varccommits($v,$a) $p] - if {$i < 0} { - puts "oops: insertfakerow can't find [shortids $p] on arc $a" - return - } - set children($v,$id) {} - set parents($v,$id) [list $p] - set varcid($v,$id) $a - lappend children($v,$p) $id - set cmitlisted($v,$id) 1 - set numcommits [incr commitidx($v)] - # note we deliberately don't update varcstart($v) even if $i == 0 - set varccommits($v,$a) [linsert $varccommits($v,$a) $i $id] - modify_arc $v $a $i - if {[info exists targetid]} { - if {![comes_before $targetid $p]} { - incr targetrow - } - } - setcanvscroll - drawvisible -} - -proc removefakerow {id} { - global varcid varccommits parents children commitidx - global varctok vtokmod cmitlisted currentid selectedline - global targetid curview numcommits - - set v $curview - if {[llength $parents($v,$id)] != 1} { - puts "oops: removefakerow [shortids $id] has [llength $parents($v,$id)] parents" - return - } - set p [lindex $parents($v,$id) 0] - set a $varcid($v,$id) - set i [lsearch -exact $varccommits($v,$a) $id] - if {$i < 0} { - puts "oops: removefakerow can't find [shortids $id] on arc $a" - return - } - unset varcid($v,$id) - set varccommits($v,$a) [lreplace $varccommits($v,$a) $i $i] - unset parents($v,$id) - unset children($v,$id) - unset cmitlisted($v,$id) - set numcommits [incr commitidx($v) -1] - set j [lsearch -exact $children($v,$p) $id] - if {$j >= 0} { - set children($v,$p) [lreplace $children($v,$p) $j $j] - } - modify_arc $v $a $i - if {[info exist currentid] && $id eq $currentid} { - unset currentid - set selectedline {} - } - if {[info exists targetid] && $targetid eq $id} { - set targetid $p - } - setcanvscroll - drawvisible -} - -proc real_children {vp} { - global children nullid nullid2 - - set kids {} - foreach id $children($vp) { - if {$id ne $nullid && $id ne $nullid2} { - lappend kids $id - } - } - return $kids -} - -proc first_real_child {vp} { - global children nullid nullid2 - - foreach id $children($vp) { - if {$id ne $nullid && $id ne $nullid2} { - return $id - } - } - return {} -} - -proc last_real_child {vp} { - global children nullid nullid2 - - set kids $children($vp) - for {set i [llength $kids]} {[incr i -1] >= 0} {} { - set id [lindex $kids $i] - if {$id ne $nullid && $id ne $nullid2} { - return $id - } - } - return {} -} - -proc vtokcmp {v a b} { - global varctok varcid - - return [string compare [lindex $varctok($v) $varcid($v,$a)] \ - [lindex $varctok($v) $varcid($v,$b)]] -} - -# This assumes that if lim is not given, the caller has checked that -# arc a's token is less than $vtokmod($v) -proc modify_arc {v a {lim {}}} { - global varctok vtokmod varcmod varcrow vupptr curview vrowmod varccommits - - if {$lim ne {}} { - set c [string compare [lindex $varctok($v) $a] $vtokmod($v)] - if {$c > 0} return - if {$c == 0} { - set r [lindex $varcrow($v) $a] - if {$r ne {} && $vrowmod($v) <= $r + $lim} return - } - } - set vtokmod($v) [lindex $varctok($v) $a] - set varcmod($v) $a - if {$v == $curview} { - while {$a != 0 && [lindex $varcrow($v) $a] eq {}} { - set a [lindex $vupptr($v) $a] - set lim {} - } - set r 0 - if {$a != 0} { - if {$lim eq {}} { - set lim [llength $varccommits($v,$a)] - } - set r [expr {[lindex $varcrow($v) $a] + $lim}] - } - set vrowmod($v) $r - undolayout $r - } -} - -proc update_arcrows {v} { - global vtokmod varcmod vrowmod varcrow commitidx currentid selectedline - global varcid vrownum varcorder varcix varccommits - global vupptr vdownptr vleftptr varctok - global displayorder parentlist curview cached_commitrow - - if {$vrowmod($v) == $commitidx($v)} return - if {$v == $curview} { - if {[llength $displayorder] > $vrowmod($v)} { - set displayorder [lrange $displayorder 0 [expr {$vrowmod($v) - 1}]] - set parentlist [lrange $parentlist 0 [expr {$vrowmod($v) - 1}]] - } - unset -nocomplain cached_commitrow - } - set narctot [expr {[llength $varctok($v)] - 1}] - set a $varcmod($v) - while {$a != 0 && [lindex $varcix($v) $a] eq {}} { - # go up the tree until we find something that has a row number, - # or we get to a seed - set a [lindex $vupptr($v) $a] - } - if {$a == 0} { - set a [lindex $vdownptr($v) 0] - if {$a == 0} return - set vrownum($v) {0} - set varcorder($v) [list $a] - lset varcix($v) $a 0 - lset varcrow($v) $a 0 - set arcn 0 - set row 0 - } else { - set arcn [lindex $varcix($v) $a] - if {[llength $vrownum($v)] > $arcn + 1} { - set vrownum($v) [lrange $vrownum($v) 0 $arcn] - set varcorder($v) [lrange $varcorder($v) 0 $arcn] - } - set row [lindex $varcrow($v) $a] - } - while {1} { - set p $a - incr row [llength $varccommits($v,$a)] - # go down if possible - set b [lindex $vdownptr($v) $a] - if {$b == 0} { - # if not, go left, or go up until we can go left - while {$a != 0} { - set b [lindex $vleftptr($v) $a] - if {$b != 0} break - set a [lindex $vupptr($v) $a] - } - if {$a == 0} break - } - set a $b - incr arcn - lappend vrownum($v) $row - lappend varcorder($v) $a - lset varcix($v) $a $arcn - lset varcrow($v) $a $row - } - set vtokmod($v) [lindex $varctok($v) $p] - set varcmod($v) $p - set vrowmod($v) $row - if {[info exists currentid]} { - set selectedline [rowofcommit $currentid] - } -} - -# Test whether view $v contains commit $id -proc commitinview {id v} { - global varcid - - return [info exists varcid($v,$id)] -} - -# Return the row number for commit $id in the current view -proc rowofcommit {id} { - global varcid varccommits varcrow curview cached_commitrow - global varctok vtokmod - - set v $curview - if {![info exists varcid($v,$id)]} { - puts "oops rowofcommit no arc for [shortids $id]" - return {} - } - set a $varcid($v,$id) - if {[string compare [lindex $varctok($v) $a] $vtokmod($v)] >= 0} { - update_arcrows $v - } - if {[info exists cached_commitrow($id)]} { - return $cached_commitrow($id) - } - set i [lsearch -exact $varccommits($v,$a) $id] - if {$i < 0} { - puts "oops didn't find commit [shortids $id] in arc $a" - return {} - } - incr i [lindex $varcrow($v) $a] - set cached_commitrow($id) $i - return $i -} - -# Returns 1 if a is on an earlier row than b, otherwise 0 -proc comes_before {a b} { - global varcid varctok curview - - set v $curview - if {$a eq $b || ![info exists varcid($v,$a)] || \ - ![info exists varcid($v,$b)]} { - return 0 - } - if {$varcid($v,$a) != $varcid($v,$b)} { - return [expr {[string compare [lindex $varctok($v) $varcid($v,$a)] \ - [lindex $varctok($v) $varcid($v,$b)]] < 0}] - } - return [expr {[rowofcommit $a] < [rowofcommit $b]}] -} - -proc bsearch {l elt} { - if {[llength $l] == 0 || $elt <= [lindex $l 0]} { - return 0 - } - set lo 0 - set hi [llength $l] - while {$hi - $lo > 1} { - set mid [expr {int(($lo + $hi) / 2)}] - set t [lindex $l $mid] - if {$elt < $t} { - set hi $mid - } elseif {$elt > $t} { - set lo $mid - } else { - return $mid - } - } - return $lo -} - -# Make sure rows $start..$end-1 are valid in displayorder and parentlist -proc make_disporder {start end} { - global vrownum curview commitidx displayorder parentlist - global varccommits varcorder parents vrowmod varcrow - global d_valid_start d_valid_end - - if {$end > $vrowmod($curview)} { - update_arcrows $curview - } - set ai [bsearch $vrownum($curview) $start] - set start [lindex $vrownum($curview) $ai] - set narc [llength $vrownum($curview)] - for {set r $start} {$ai < $narc && $r < $end} {incr ai} { - set a [lindex $varcorder($curview) $ai] - set l [llength $displayorder] - set al [llength $varccommits($curview,$a)] - if {$l < $r + $al} { - if {$l < $r} { - set pad [ntimes [expr {$r - $l}] {}] - set displayorder [concat $displayorder $pad] - set parentlist [concat $parentlist $pad] - } elseif {$l > $r} { - set displayorder [lrange $displayorder 0 [expr {$r - 1}]] - set parentlist [lrange $parentlist 0 [expr {$r - 1}]] - } - foreach id $varccommits($curview,$a) { - lappend displayorder $id - lappend parentlist $parents($curview,$id) - } - } elseif {[lindex $displayorder [expr {$r + $al - 1}]] eq {}} { - set i $r - foreach id $varccommits($curview,$a) { - lset displayorder $i $id - lset parentlist $i $parents($curview,$id) - incr i - } - } - incr r $al - } -} - -proc commitonrow {row} { - global displayorder - - set id [lindex $displayorder $row] - if {$id eq {}} { - make_disporder $row [expr {$row + 1}] - set id [lindex $displayorder $row] - } - return $id -} - -proc closevarcs {v} { - global varctok varccommits varcid parents children - global cmitlisted commitidx vtokmod curview numcommits - - set missing_parents 0 - set scripts {} - set narcs [llength $varctok($v)] - for {set a 1} {$a < $narcs} {incr a} { - set id [lindex $varccommits($v,$a) end] - foreach p $parents($v,$id) { - if {[info exists varcid($v,$p)]} continue - # add p as a new commit - incr missing_parents - set cmitlisted($v,$p) 0 - set parents($v,$p) {} - if {[llength $children($v,$p)] == 1 && - [llength $parents($v,$id)] == 1} { - set b $a - } else { - set b [newvarc $v $p] - } - set varcid($v,$p) $b - if {[string compare [lindex $varctok($v) $b] $vtokmod($v)] < 0} { - modify_arc $v $b - } - lappend varccommits($v,$b) $p - incr commitidx($v) - if {$v == $curview} { - set numcommits $commitidx($v) - } - set scripts [check_interest $p $scripts] - } - } - if {$missing_parents > 0} { - foreach s $scripts { - eval $s - } - } -} - -# Use $rwid as a substitute for $id, i.e. reparent $id's children to $rwid -# Assumes we already have an arc for $rwid. -proc rewrite_commit {v id rwid} { - global children parents varcid varctok vtokmod varccommits - - foreach ch $children($v,$id) { - # make $rwid be $ch's parent in place of $id - set i [lsearch -exact $parents($v,$ch) $id] - if {$i < 0} { - puts "oops rewrite_commit didn't find $id in parent list for $ch" - } - set parents($v,$ch) [lreplace $parents($v,$ch) $i $i $rwid] - # add $ch to $rwid's children and sort the list if necessary - if {[llength [lappend children($v,$rwid) $ch]] > 1} { - set children($v,$rwid) [lsort -command [list vtokcmp $v] \ - $children($v,$rwid)] - } - # fix the graph after joining $id to $rwid - set a $varcid($v,$ch) - fix_reversal $rwid $a $v - # parentlist is wrong for the last element of arc $a - # even if displayorder is right, hence the 3rd arg here - modify_arc $v $a [expr {[llength $varccommits($v,$a)] - 1}] - } -} - -# Mechanism for registering a command to be executed when we come -# across a particular commit. To handle the case when only the -# prefix of the commit is known, the commitinterest array is now -# indexed by the first 4 characters of the ID. Each element is a -# list of id, cmd pairs. -proc interestedin {id cmd} { - global commitinterest - - lappend commitinterest([string range $id 0 3]) $id $cmd -} - -proc check_interest {id scripts} { - global commitinterest - - set prefix [string range $id 0 3] - if {[info exists commitinterest($prefix)]} { - set newlist {} - foreach {i script} $commitinterest($prefix) { - if {[string match "$i*" $id]} { - lappend scripts [string map [list "%I" $id "%P" $i] $script] - } else { - lappend newlist $i $script - } - } - if {$newlist ne {}} { - set commitinterest($prefix) $newlist - } else { - unset commitinterest($prefix) - } - } - return $scripts -} - -proc getcommitlines {fd inst view updating} { - global cmitlisted leftover - global commitidx commitdata vdatemode - global parents children curview hlview - global idpending ordertok - global varccommits varcid varctok vtokmod vfilelimit vshortids - - set stuff [read $fd 500000] - # git log doesn't terminate the last commit with a null... - if {$stuff == {} && $leftover($inst) ne {} && [eof $fd]} { - set stuff "\0" - } - if {$stuff == {}} { - if {![eof $fd]} { - return 1 - } - global commfd viewcomplete viewactive viewname - global viewinstances - unset commfd($inst) - set i [lsearch -exact $viewinstances($view) $inst] - if {$i >= 0} { - set viewinstances($view) [lreplace $viewinstances($view) $i $i] - } - # set it blocking so we wait for the process to terminate - fconfigure $fd -blocking 1 - if {[catch {close $fd} err]} { - set fv {} - if {$view != $curview} { - set fv " for the \"$viewname($view)\" view" - } - if {[string range $err 0 4] == "usage"} { - set err "Gitk: error reading commits$fv:\ - bad arguments to git log." - if {$viewname($view) eq [mc "Command line"]} { - append err \ - " (Note: arguments to gitk are passed to git log\ - to allow selection of commits to be displayed.)" - } - } else { - set err "Error reading commits$fv: $err" - } - error_popup $err - } - if {[incr viewactive($view) -1] <= 0} { - set viewcomplete($view) 1 - # Check if we have seen any ids listed as parents that haven't - # appeared in the list - closevarcs $view - notbusy $view - } - if {$view == $curview} { - run chewcommits - } - return 0 - } - set start 0 - set gotsome 0 - set scripts {} - while 1 { - set i [string first "\0" $stuff $start] - if {$i < 0} { - append leftover($inst) [string range $stuff $start end] - break - } - if {$start == 0} { - set cmit $leftover($inst) - append cmit [string range $stuff 0 [expr {$i - 1}]] - set leftover($inst) {} - } else { - set cmit [string range $stuff $start [expr {$i - 1}]] - } - set start [expr {$i + 1}] - set j [string first "\n" $cmit] - set ok 0 - set listed 1 - if {$j >= 0 && [string match "commit *" $cmit]} { - set ids [string range $cmit 7 [expr {$j - 1}]] - if {[string match {[-^<>]*} $ids]} { - switch -- [string index $ids 0] { - "-" {set listed 0} - "^" {set listed 2} - "<" {set listed 3} - ">" {set listed 4} - } - set ids [string range $ids 1 end] - } - set ok 1 - foreach id $ids { - if {[string length $id] != 40} { - set ok 0 - break - } - } - } - if {!$ok} { - set shortcmit $cmit - if {[string length $shortcmit] > 80} { - set shortcmit "[string range $shortcmit 0 80]..." - } - error_popup "[mc "Can't parse git log output:"] {$shortcmit}" - exit 1 - } - set id [lindex $ids 0] - set vid $view,$id - - lappend vshortids($view,[string range $id 0 3]) $id - - if {!$listed && $updating && ![info exists varcid($vid)] && - $vfilelimit($view) ne {}} { - # git log doesn't rewrite parents for unlisted commits - # when doing path limiting, so work around that here - # by working out the rewritten parent with git rev-list - # and if we already know about it, using the rewritten - # parent as a substitute parent for $id's children. - if {![catch { - set rwid [exec git rev-list --first-parent --max-count=1 \ - $id -- $vfilelimit($view)] - }]} { - if {$rwid ne {} && [info exists varcid($view,$rwid)]} { - # use $rwid in place of $id - rewrite_commit $view $id $rwid - continue - } - } - } - - set a 0 - if {[info exists varcid($vid)]} { - if {$cmitlisted($vid) || !$listed} continue - set a $varcid($vid) - } - if {$listed} { - set olds [lrange $ids 1 end] - } else { - set olds {} - } - set commitdata($id) [string range $cmit [expr {$j + 1}] end] - set cmitlisted($vid) $listed - set parents($vid) $olds - if {![info exists children($vid)]} { - set children($vid) {} - } elseif {$a == 0 && [llength $children($vid)] == 1} { - set k [lindex $children($vid) 0] - if {[llength $parents($view,$k)] == 1 && - (!$vdatemode($view) || - $varcid($view,$k) == [llength $varctok($view)] - 1)} { - set a $varcid($view,$k) - } - } - if {$a == 0} { - # new arc - set a [newvarc $view $id] - } - if {[string compare [lindex $varctok($view) $a] $vtokmod($view)] < 0} { - modify_arc $view $a - } - if {![info exists varcid($vid)]} { - set varcid($vid) $a - lappend varccommits($view,$a) $id - incr commitidx($view) - } - - set i 0 - foreach p $olds { - if {$i == 0 || [lsearch -exact $olds $p] >= $i} { - set vp $view,$p - if {[llength [lappend children($vp) $id]] > 1 && - [vtokcmp $view [lindex $children($vp) end-1] $id] > 0} { - set children($vp) [lsort -command [list vtokcmp $view] \ - $children($vp)] - unset -nocomplain ordertok - } - if {[info exists varcid($view,$p)]} { - fix_reversal $p $a $view - } - } - incr i - } - - set scripts [check_interest $id $scripts] - set gotsome 1 - } - if {$gotsome} { - global numcommits hlview - - if {$view == $curview} { - set numcommits $commitidx($view) - run chewcommits - } - if {[info exists hlview] && $view == $hlview} { - # we never actually get here... - run vhighlightmore - } - foreach s $scripts { - eval $s - } - } - return 2 -} - -proc chewcommits {} { - global curview hlview viewcomplete - global pending_select - - layoutmore - if {$viewcomplete($curview)} { - global commitidx varctok - global numcommits startmsecs - - if {[info exists pending_select]} { - update - reset_pending_select {} - - if {[commitinview $pending_select $curview]} { - selectline [rowofcommit $pending_select] 1 - } else { - set row [first_real_row] - selectline $row 1 - } - } - if {$commitidx($curview) > 0} { - #set ms [expr {[clock clicks -milliseconds] - $startmsecs}] - #puts "overall $ms ms for $numcommits commits" - #puts "[llength $varctok($view)] arcs, $commitidx($view) commits" - } else { - show_status [mc "No commits selected"] - } - notbusy layout - } - return 0 -} - -proc do_readcommit {id} { - global tclencoding - - # Invoke git-log to handle automatic encoding conversion - set fd [open [concat | git log --no-color --pretty=raw -1 $id] r] - # Read the results using i18n.logoutputencoding - fconfigure $fd -translation lf -eofchar {} - if {$tclencoding != {}} { - fconfigure $fd -encoding $tclencoding - } - set contents [read $fd] - close $fd - # Remove the heading line - regsub {^commit [0-9a-f]+\n} $contents {} contents - - return $contents -} - -proc readcommit {id} { - if {[catch {set contents [do_readcommit $id]}]} return - parsecommit $id $contents 1 -} - -proc parsecommit {id contents listed} { - global commitinfo - - set inhdr 1 - set comment {} - set headline {} - set auname {} - set audate {} - set comname {} - set comdate {} - set hdrend [string first "\n\n" $contents] - if {$hdrend < 0} { - # should never happen... - set hdrend [string length $contents] - } - set header [string range $contents 0 [expr {$hdrend - 1}]] - set comment [string range $contents [expr {$hdrend + 2}] end] - foreach line [split $header "\n"] { - set line [split $line " "] - set tag [lindex $line 0] - if {$tag == "author"} { - set audate [lrange $line end-1 end] - set auname [join [lrange $line 1 end-2] " "] - } elseif {$tag == "committer"} { - set comdate [lrange $line end-1 end] - set comname [join [lrange $line 1 end-2] " "] - } - } - set headline {} - # take the first non-blank line of the comment as the headline - set headline [string trimleft $comment] - set i [string first "\n" $headline] - if {$i >= 0} { - set headline [string range $headline 0 $i] - } - set headline [string trimright $headline] - set i [string first "\r" $headline] - if {$i >= 0} { - set headline [string trimright [string range $headline 0 $i]] - } - if {!$listed} { - # git log indents the comment by 4 spaces; - # if we got this via git cat-file, add the indentation - set newcomment {} - foreach line [split $comment "\n"] { - append newcomment " " - append newcomment $line - append newcomment "\n" - } - set comment $newcomment - } - set hasnote [string first "\nNotes:\n" $contents] - set diff "" - # If there is diff output shown in the git-log stream, split it - # out. But get rid of the empty line that always precedes the - # diff. - set i [string first "\n\ndiff" $comment] - if {$i >= 0} { - set diff [string range $comment $i+1 end] - set comment [string range $comment 0 $i-1] - } - set commitinfo($id) [list $headline $auname $audate \ - $comname $comdate $comment $hasnote $diff] -} - -proc getcommit {id} { - global commitdata commitinfo - - if {[info exists commitdata($id)]} { - parsecommit $id $commitdata($id) 1 - } else { - readcommit $id - if {![info exists commitinfo($id)]} { - set commitinfo($id) [list [mc "No commit information available"]] - } - } - return 1 -} - -# Expand an abbreviated commit ID to a list of full 40-char IDs that match -# and are present in the current view. -# This is fairly slow... -proc longid {prefix} { - global varcid curview vshortids - - set ids {} - if {[string length $prefix] >= 4} { - set vshortid $curview,[string range $prefix 0 3] - if {[info exists vshortids($vshortid)]} { - foreach id $vshortids($vshortid) { - if {[string match "$prefix*" $id]} { - if {[lsearch -exact $ids $id] < 0} { - lappend ids $id - if {[llength $ids] >= 2} break - } - } - } - } - } else { - foreach match [array names varcid "$curview,$prefix*"] { - lappend ids [lindex [split $match ","] 1] - if {[llength $ids] >= 2} break - } - } - return $ids -} - -proc readrefs {} { - global tagids idtags headids idheads tagobjid - global otherrefids idotherrefs mainhead mainheadid - global selecthead selectheadid - global hideremotes - - foreach v {tagids idtags headids idheads otherrefids idotherrefs} { - unset -nocomplain $v - } - set refd [open [list | git show-ref -d] r] - while {[gets $refd line] >= 0} { - if {[string index $line 40] ne " "} continue - set id [string range $line 0 39] - set ref [string range $line 41 end] - if {![string match "refs/*" $ref]} continue - set name [string range $ref 5 end] - if {[string match "remotes/*" $name]} { - if {![string match "*/HEAD" $name] && !$hideremotes} { - set headids($name) $id - lappend idheads($id) $name - } - } elseif {[string match "heads/*" $name]} { - set name [string range $name 6 end] - set headids($name) $id - lappend idheads($id) $name - } elseif {[string match "tags/*" $name]} { - # this lets refs/tags/foo^{} overwrite refs/tags/foo, - # which is what we want since the former is the commit ID - set name [string range $name 5 end] - if {[string match "*^{}" $name]} { - set name [string range $name 0 end-3] - } else { - set tagobjid($name) $id - } - set tagids($name) $id - lappend idtags($id) $name - } else { - set otherrefids($name) $id - lappend idotherrefs($id) $name - } - } - catch {close $refd} - set mainhead {} - set mainheadid {} - catch { - set mainheadid [exec git rev-parse HEAD] - set thehead [exec git symbolic-ref HEAD] - if {[string match "refs/heads/*" $thehead]} { - set mainhead [string range $thehead 11 end] - } - } - set selectheadid {} - if {$selecthead ne {}} { - catch { - set selectheadid [exec git rev-parse --verify $selecthead] - } - } -} - -# skip over fake commits -proc first_real_row {} { - global nullid nullid2 numcommits - - for {set row 0} {$row < $numcommits} {incr row} { - set id [commitonrow $row] - if {$id ne $nullid && $id ne $nullid2} { - break - } - } - return $row -} - -# update things for a head moved to a child of its previous location -proc movehead {id name} { - global headids idheads - - removehead $headids($name) $name - set headids($name) $id - lappend idheads($id) $name -} - -# update things when a head has been removed -proc removehead {id name} { - global headids idheads - - if {$idheads($id) eq $name} { - unset idheads($id) - } else { - set i [lsearch -exact $idheads($id) $name] - if {$i >= 0} { - set idheads($id) [lreplace $idheads($id) $i $i] - } - } - unset headids($name) -} - -proc ttk_toplevel {w args} { - global use_ttk - eval [linsert $args 0 ::toplevel $w] - if {$use_ttk} { - place [ttk::frame $w._toplevel_background] -x 0 -y 0 -relwidth 1 -relheight 1 - } - return $w -} - -proc make_transient {window origin} { - global have_tk85 - - # In MacOS Tk 8.4 transient appears to work by setting - # overrideredirect, which is utterly useless, since the - # windows get no border, and are not even kept above - # the parent. - if {!$have_tk85 && [tk windowingsystem] eq {aqua}} return - - wm transient $window $origin - - # Windows fails to place transient windows normally, so - # schedule a callback to center them on the parent. - if {[tk windowingsystem] eq {win32}} { - after idle [list tk::PlaceWindow $window widget $origin] - } -} - -proc show_error {w top msg} { - global NS - if {![info exists NS]} {set NS ""} - if {[wm state $top] eq "withdrawn"} { wm deiconify $top } - message $w.m -text $msg -justify center -aspect 400 - pack $w.m -side top -fill x -padx 20 -pady 20 - ${NS}::button $w.ok -default active -text [mc OK] -command "destroy $top" - pack $w.ok -side bottom -fill x - bind $top "grab $top; focus $top" - bind $top "destroy $top" - bind $top "destroy $top" - bind $top "destroy $top" - tkwait window $top -} - -proc error_popup {msg {owner .}} { - if {[tk windowingsystem] eq "win32"} { - tk_messageBox -icon error -type ok -title [wm title .] \ - -parent $owner -message $msg - } else { - set w .error - ttk_toplevel $w - make_transient $w $owner - show_error $w $w $msg - } -} - -proc confirm_popup {msg {owner .}} { - global confirm_ok NS - set confirm_ok 0 - set w .confirm - ttk_toplevel $w - make_transient $w $owner - message $w.m -text $msg -justify center -aspect 400 - pack $w.m -side top -fill x -padx 20 -pady 20 - ${NS}::button $w.ok -text [mc OK] -command "set confirm_ok 1; destroy $w" - pack $w.ok -side left -fill x - ${NS}::button $w.cancel -text [mc Cancel] -command "destroy $w" - pack $w.cancel -side right -fill x - bind $w "grab $w; focus $w" - bind $w "set confirm_ok 1; destroy $w" - bind $w "set confirm_ok 1; destroy $w" - bind $w "destroy $w" - tk::PlaceWindow $w widget $owner - tkwait window $w - return $confirm_ok -} - -proc setoptions {} { - global use_ttk - - if {[tk windowingsystem] ne "win32"} { - option add *Panedwindow.showHandle 1 startupFile - option add *Panedwindow.sashRelief raised startupFile - if {[tk windowingsystem] ne "aqua"} { - option add *Menu.font uifont startupFile - } - } else { - option add *Menu.TearOff 0 startupFile - } - option add *Button.font uifont startupFile - option add *Checkbutton.font uifont startupFile - option add *Radiobutton.font uifont startupFile - option add *Menubutton.font uifont startupFile - option add *Label.font uifont startupFile - option add *Message.font uifont startupFile - option add *Entry.font textfont startupFile - option add *Text.font textfont startupFile - option add *Labelframe.font uifont startupFile - option add *Spinbox.font textfont startupFile - option add *Listbox.font mainfont startupFile -} - -proc setttkstyle {} { - eval font configure TkDefaultFont [fontflags mainfont] - eval font configure TkTextFont [fontflags textfont] - eval font configure TkHeadingFont [fontflags mainfont] - eval font configure TkCaptionFont [fontflags mainfont] -weight bold - eval font configure TkTooltipFont [fontflags uifont] - eval font configure TkFixedFont [fontflags textfont] - eval font configure TkIconFont [fontflags uifont] - eval font configure TkMenuFont [fontflags uifont] - eval font configure TkSmallCaptionFont [fontflags uifont] -} - -# Make a menu and submenus. -# m is the window name for the menu, items is the list of menu items to add. -# Each item is a list {mc label type description options...} -# mc is ignored; it's so we can put mc there to alert xgettext -# label is the string that appears in the menu -# type is cascade, command or radiobutton (should add checkbutton) -# description depends on type; it's the sublist for cascade, the -# command to invoke for command, or {variable value} for radiobutton -proc makemenu {m items} { - menu $m - if {[tk windowingsystem] eq {aqua}} { - set Meta1 Cmd - } else { - set Meta1 Ctrl - } - foreach i $items { - set name [mc [lindex $i 1]] - set type [lindex $i 2] - set thing [lindex $i 3] - set params [list $type] - if {$name ne {}} { - set u [string first "&" [string map {&& x} $name]] - lappend params -label [string map {&& & & {}} $name] - if {$u >= 0} { - lappend params -underline $u - } - } - switch -- $type { - "cascade" { - set submenu [string tolower [string map {& ""} [lindex $i 1]]] - lappend params -menu $m.$submenu - } - "command" { - lappend params -command $thing - } - "radiobutton" { - lappend params -variable [lindex $thing 0] \ - -value [lindex $thing 1] - } - } - set tail [lrange $i 4 end] - regsub -all {\yMeta1\y} $tail $Meta1 tail - eval $m add $params $tail - if {$type eq "cascade"} { - makemenu $m.$submenu $thing - } - } -} - -# translate string and remove ampersands -proc mca {str} { - return [string map {&& & & {}} [mc $str]] -} - -proc cleardropsel {w} { - $w selection clear -} -proc makedroplist {w varname args} { - global use_ttk - if {$use_ttk} { - set width 0 - foreach label $args { - set cx [string length $label] - if {$cx > $width} {set width $cx} - } - set gm [ttk::combobox $w -width $width -state readonly\ - -textvariable $varname -values $args \ - -exportselection false] - bind $gm <> [list $gm selection clear] - } else { - set gm [eval [linsert $args 0 tk_optionMenu $w $varname]] - } - return $gm -} - -proc makewindow {} { - global canv canv2 canv3 linespc charspc ctext cflist cscroll - global tabstop - global findtype findtypemenu findloc findstring fstring geometry - global entries sha1entry sha1string sha1but - global diffcontextstring diffcontext - global ignorespace - global maincursor textcursor curtextcursor - global rowctxmenu fakerowmenu mergemax wrapcomment - global highlight_files gdttype - global searchstring sstring - global bgcolor fgcolor bglist fglist diffcolors selectbgcolor - global uifgcolor uifgdisabledcolor - global filesepbgcolor filesepfgcolor - global mergecolors foundbgcolor currentsearchhitbgcolor - global headctxmenu progresscanv progressitem progresscoords statusw - global fprogitem fprogcoord lastprogupdate progupdatepending - global rprogitem rprogcoord rownumsel numcommits - global have_tk85 use_ttk NS - global git_version - global worddiff - - # The "mc" arguments here are purely so that xgettext - # sees the following string as needing to be translated - set file { - mc "&File" cascade { - {mc "&Update" command updatecommits -accelerator F5} - {mc "&Reload" command reloadcommits -accelerator Shift-F5} - {mc "Reread re&ferences" command rereadrefs} - {mc "&List references" command showrefs -accelerator F2} - {xx "" separator} - {mc "Start git &gui" command {exec git gui &}} - {xx "" separator} - {mc "&Quit" command doquit -accelerator Meta1-Q} - }} - set edit { - mc "&Edit" cascade { - {mc "&Preferences" command doprefs} - }} - set view { - mc "&View" cascade { - {mc "&New view..." command {newview 0} -accelerator Shift-F4} - {mc "&Edit view..." command editview -state disabled -accelerator F4} - {mc "&Delete view" command delview -state disabled} - {xx "" separator} - {mc "&All files" radiobutton {selectedview 0} -command {showview 0}} - }} - if {[tk windowingsystem] ne "aqua"} { - set help { - mc "&Help" cascade { - {mc "&About gitk" command about} - {mc "&Key bindings" command keys} - }} - set bar [list $file $edit $view $help] - } else { - proc ::tk::mac::ShowPreferences {} {doprefs} - proc ::tk::mac::Quit {} {doquit} - lset file end [lreplace [lindex $file end] end-1 end] - set apple { - xx "&Apple" cascade { - {mc "&About gitk" command about} - {xx "" separator} - }} - set help { - mc "&Help" cascade { - {mc "&Key bindings" command keys} - }} - set bar [list $apple $file $view $help] - } - makemenu .bar $bar - . configure -menu .bar - - if {$use_ttk} { - # cover the non-themed toplevel with a themed frame. - place [ttk::frame ._main_background] -x 0 -y 0 -relwidth 1 -relheight 1 - } - - # the gui has upper and lower half, parts of a paned window. - ${NS}::panedwindow .ctop -orient vertical - - # possibly use assumed geometry - if {![info exists geometry(pwsash0)]} { - set geometry(topheight) [expr {15 * $linespc}] - set geometry(topwidth) [expr {80 * $charspc}] - set geometry(botheight) [expr {15 * $linespc}] - set geometry(botwidth) [expr {50 * $charspc}] - set geometry(pwsash0) [list [expr {40 * $charspc}] 2] - set geometry(pwsash1) [list [expr {60 * $charspc}] 2] - } - - # the upper half will have a paned window, a scroll bar to the right, and some stuff below - ${NS}::frame .tf -height $geometry(topheight) -width $geometry(topwidth) - ${NS}::frame .tf.histframe - ${NS}::panedwindow .tf.histframe.pwclist -orient horizontal - if {!$use_ttk} { - .tf.histframe.pwclist configure -sashpad 0 -handlesize 4 - } - - # create three canvases - set cscroll .tf.histframe.csb - set canv .tf.histframe.pwclist.canv - canvas $canv \ - -selectbackground $selectbgcolor \ - -background $bgcolor -bd 0 \ - -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll" - .tf.histframe.pwclist add $canv - set canv2 .tf.histframe.pwclist.canv2 - canvas $canv2 \ - -selectbackground $selectbgcolor \ - -background $bgcolor -bd 0 -yscrollincr $linespc - .tf.histframe.pwclist add $canv2 - set canv3 .tf.histframe.pwclist.canv3 - canvas $canv3 \ - -selectbackground $selectbgcolor \ - -background $bgcolor -bd 0 -yscrollincr $linespc - .tf.histframe.pwclist add $canv3 - if {$use_ttk} { - bind .tf.histframe.pwclist { - bind %W {} - .tf.histframe.pwclist sashpos 1 [lindex $::geometry(pwsash1) 0] - .tf.histframe.pwclist sashpos 0 [lindex $::geometry(pwsash0) 0] - } - } else { - eval .tf.histframe.pwclist sash place 0 $geometry(pwsash0) - eval .tf.histframe.pwclist sash place 1 $geometry(pwsash1) - } - - # a scroll bar to rule them - ${NS}::scrollbar $cscroll -command {allcanvs yview} - if {!$use_ttk} {$cscroll configure -highlightthickness 0} - pack $cscroll -side right -fill y - bind .tf.histframe.pwclist {resizeclistpanes %W %w} - lappend bglist $canv $canv2 $canv3 - pack .tf.histframe.pwclist -fill both -expand 1 -side left - - # we have two button bars at bottom of top frame. Bar 1 - ${NS}::frame .tf.bar - ${NS}::frame .tf.lbar -height 15 - - set sha1entry .tf.bar.sha1 - set entries $sha1entry - set sha1but .tf.bar.sha1label - button $sha1but -text "[mc "SHA1 ID:"] " -state disabled -relief flat \ - -command gotocommit -width 8 - $sha1but conf -disabledforeground [$sha1but cget -foreground] - pack .tf.bar.sha1label -side left - ${NS}::entry $sha1entry -width 40 -font textfont -textvariable sha1string - trace add variable sha1string write sha1change - pack $sha1entry -side left -pady 2 - - set bm_left_data { - #define left_width 16 - #define left_height 16 - static unsigned char left_bits[] = { - 0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00, - 0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00, - 0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01}; - } - set bm_right_data { - #define right_width 16 - #define right_height 16 - static unsigned char right_bits[] = { - 0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c, - 0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c, - 0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01}; - } - image create bitmap bm-left -data $bm_left_data -foreground $uifgcolor - image create bitmap bm-left-gray -data $bm_left_data -foreground $uifgdisabledcolor - image create bitmap bm-right -data $bm_right_data -foreground $uifgcolor - image create bitmap bm-right-gray -data $bm_right_data -foreground $uifgdisabledcolor - - ${NS}::button .tf.bar.leftbut -command goback -state disabled -width 26 - if {$use_ttk} { - .tf.bar.leftbut configure -image [list bm-left disabled bm-left-gray] - } else { - .tf.bar.leftbut configure -image bm-left - } - pack .tf.bar.leftbut -side left -fill y - ${NS}::button .tf.bar.rightbut -command goforw -state disabled -width 26 - if {$use_ttk} { - .tf.bar.rightbut configure -image [list bm-right disabled bm-right-gray] - } else { - .tf.bar.rightbut configure -image bm-right - } - pack .tf.bar.rightbut -side left -fill y - - ${NS}::label .tf.bar.rowlabel -text [mc "Row"] - set rownumsel {} - ${NS}::label .tf.bar.rownum -width 7 -textvariable rownumsel \ - -relief sunken -anchor e - ${NS}::label .tf.bar.rowlabel2 -text "/" - ${NS}::label .tf.bar.numcommits -width 7 -textvariable numcommits \ - -relief sunken -anchor e - pack .tf.bar.rowlabel .tf.bar.rownum .tf.bar.rowlabel2 .tf.bar.numcommits \ - -side left - if {!$use_ttk} { - foreach w {rownum numcommits} {.tf.bar.$w configure -font textfont} - } - global selectedline - trace add variable selectedline write selectedline_change - - # Status label and progress bar - set statusw .tf.bar.status - ${NS}::label $statusw -width 15 -relief sunken - pack $statusw -side left -padx 5 - if {$use_ttk} { - set progresscanv [ttk::progressbar .tf.bar.progress] - } else { - set h [expr {[font metrics uifont -linespace] + 2}] - set progresscanv .tf.bar.progress - canvas $progresscanv -relief sunken -height $h -borderwidth 2 - set progressitem [$progresscanv create rect -1 0 0 $h -fill "#00ff00"] - set fprogitem [$progresscanv create rect -1 0 0 $h -fill yellow] - set rprogitem [$progresscanv create rect -1 0 0 $h -fill red] - } - pack $progresscanv -side right -expand 1 -fill x -padx {0 2} - set progresscoords {0 0} - set fprogcoord 0 - set rprogcoord 0 - bind $progresscanv adjustprogress - set lastprogupdate [clock clicks -milliseconds] - set progupdatepending 0 - - # build up the bottom bar of upper window - ${NS}::label .tf.lbar.flabel -text "[mc "Find"] " - - set bm_down_data { - #define down_width 16 - #define down_height 16 - static unsigned char down_bits[] = { - 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, - 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, - 0x87, 0xe1, 0x8e, 0x71, 0x9c, 0x39, 0xb8, 0x1d, - 0xf0, 0x0f, 0xe0, 0x07, 0xc0, 0x03, 0x80, 0x01}; - } - image create bitmap bm-down -data $bm_down_data -foreground $uifgcolor - ${NS}::button .tf.lbar.fnext -width 26 -command {dofind 1 1} - .tf.lbar.fnext configure -image bm-down - - set bm_up_data { - #define up_width 16 - #define up_height 16 - static unsigned char up_bits[] = { - 0x80, 0x01, 0xc0, 0x03, 0xe0, 0x07, 0xf0, 0x0f, - 0xb8, 0x1d, 0x9c, 0x39, 0x8e, 0x71, 0x87, 0xe1, - 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, - 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01}; - } - image create bitmap bm-up -data $bm_up_data -foreground $uifgcolor - ${NS}::button .tf.lbar.fprev -width 26 -command {dofind -1 1} - .tf.lbar.fprev configure -image bm-up - - ${NS}::label .tf.lbar.flab2 -text " [mc "commit"] " - - pack .tf.lbar.flabel .tf.lbar.fnext .tf.lbar.fprev .tf.lbar.flab2 \ - -side left -fill y - set gdttype [mc "containing:"] - set gm [makedroplist .tf.lbar.gdttype gdttype \ - [mc "containing:"] \ - [mc "touching paths:"] \ - [mc "adding/removing string:"] \ - [mc "changing lines matching:"]] - trace add variable gdttype write gdttype_change - pack .tf.lbar.gdttype -side left -fill y - - set findstring {} - set fstring .tf.lbar.findstring - lappend entries $fstring - ${NS}::entry $fstring -width 30 -textvariable findstring - trace add variable findstring write find_change - set findtype [mc "Exact"] - set findtypemenu [makedroplist .tf.lbar.findtype \ - findtype [mc "Exact"] [mc "IgnCase"] [mc "Regexp"]] - trace add variable findtype write findcom_change - set findloc [mc "All fields"] - makedroplist .tf.lbar.findloc findloc [mc "All fields"] [mc "Headline"] \ - [mc "Comments"] [mc "Author"] [mc "Committer"] - trace add variable findloc write find_change - pack .tf.lbar.findloc -side right - pack .tf.lbar.findtype -side right - pack $fstring -side left -expand 1 -fill x - - # Finish putting the upper half of the viewer together - pack .tf.lbar -in .tf -side bottom -fill x - pack .tf.bar -in .tf -side bottom -fill x - pack .tf.histframe -fill both -side top -expand 1 - .ctop add .tf - if {!$use_ttk} { - .ctop paneconfigure .tf -height $geometry(topheight) - .ctop paneconfigure .tf -width $geometry(topwidth) - } - - # now build up the bottom - ${NS}::panedwindow .pwbottom -orient horizontal - - # lower left, a text box over search bar, scroll bar to the right - # if we know window height, then that will set the lower text height, otherwise - # we set lower text height which will drive window height - if {[info exists geometry(main)]} { - ${NS}::frame .bleft -width $geometry(botwidth) - } else { - ${NS}::frame .bleft -width $geometry(botwidth) -height $geometry(botheight) - } - ${NS}::frame .bleft.top - ${NS}::frame .bleft.mid - ${NS}::frame .bleft.bottom - - # gap between sub-widgets - set wgap [font measure uifont "i"] - - ${NS}::button .bleft.top.search -text [mc "Search"] -command dosearch - pack .bleft.top.search -side left -padx 5 - set sstring .bleft.top.sstring - set searchstring "" - ${NS}::entry $sstring -width 20 -textvariable searchstring - lappend entries $sstring - trace add variable searchstring write incrsearch - pack $sstring -side left -expand 1 -fill x - ${NS}::radiobutton .bleft.mid.diff -text [mc "Diff"] \ - -command changediffdisp -variable diffelide -value {0 0} - ${NS}::radiobutton .bleft.mid.old -text [mc "Old version"] \ - -command changediffdisp -variable diffelide -value {0 1} - ${NS}::radiobutton .bleft.mid.new -text [mc "New version"] \ - -command changediffdisp -variable diffelide -value {1 0} - - ${NS}::label .bleft.mid.labeldiffcontext -text " [mc "Lines of context"]: " - pack .bleft.mid.diff .bleft.mid.old .bleft.mid.new -side left -ipadx $wgap - spinbox .bleft.mid.diffcontext -width 5 \ - -from 0 -increment 1 -to 10000000 \ - -validate all -validatecommand "diffcontextvalidate %P" \ - -textvariable diffcontextstring - .bleft.mid.diffcontext set $diffcontext - trace add variable diffcontextstring write diffcontextchange - lappend entries .bleft.mid.diffcontext - pack .bleft.mid.labeldiffcontext .bleft.mid.diffcontext -side left -ipadx $wgap - ${NS}::checkbutton .bleft.mid.ignspace -text [mc "Ignore space change"] \ - -command changeignorespace -variable ignorespace - pack .bleft.mid.ignspace -side left -padx 5 - - set worddiff [mc "Line diff"] - if {[package vcompare $git_version "1.7.2"] >= 0} { - makedroplist .bleft.mid.worddiff worddiff [mc "Line diff"] \ - [mc "Markup words"] [mc "Color words"] - trace add variable worddiff write changeworddiff - pack .bleft.mid.worddiff -side left -padx 5 - } - - set ctext .bleft.bottom.ctext - text $ctext -background $bgcolor -foreground $fgcolor \ - -state disabled -undo 0 -font textfont \ - -yscrollcommand scrolltext -wrap none \ - -xscrollcommand ".bleft.bottom.sbhorizontal set" - if {$have_tk85} { - $ctext conf -tabstyle wordprocessor - } - ${NS}::scrollbar .bleft.bottom.sb -command "$ctext yview" - ${NS}::scrollbar .bleft.bottom.sbhorizontal -command "$ctext xview" -orient h - pack .bleft.top -side top -fill x - pack .bleft.mid -side top -fill x - grid $ctext .bleft.bottom.sb -sticky nsew - grid .bleft.bottom.sbhorizontal -sticky ew - grid columnconfigure .bleft.bottom 0 -weight 1 - grid rowconfigure .bleft.bottom 0 -weight 1 - grid rowconfigure .bleft.bottom 1 -weight 0 - pack .bleft.bottom -side top -fill both -expand 1 - lappend bglist $ctext - lappend fglist $ctext - - $ctext tag conf comment -wrap $wrapcomment - $ctext tag conf filesep -font textfontbold -fore $filesepfgcolor -back $filesepbgcolor - $ctext tag conf hunksep -fore [lindex $diffcolors 2] - $ctext tag conf d0 -fore [lindex $diffcolors 0] - $ctext tag conf dresult -fore [lindex $diffcolors 1] - $ctext tag conf m0 -fore [lindex $mergecolors 0] - $ctext tag conf m1 -fore [lindex $mergecolors 1] - $ctext tag conf m2 -fore [lindex $mergecolors 2] - $ctext tag conf m3 -fore [lindex $mergecolors 3] - $ctext tag conf m4 -fore [lindex $mergecolors 4] - $ctext tag conf m5 -fore [lindex $mergecolors 5] - $ctext tag conf m6 -fore [lindex $mergecolors 6] - $ctext tag conf m7 -fore [lindex $mergecolors 7] - $ctext tag conf m8 -fore [lindex $mergecolors 8] - $ctext tag conf m9 -fore [lindex $mergecolors 9] - $ctext tag conf m10 -fore [lindex $mergecolors 10] - $ctext tag conf m11 -fore [lindex $mergecolors 11] - $ctext tag conf m12 -fore [lindex $mergecolors 12] - $ctext tag conf m13 -fore [lindex $mergecolors 13] - $ctext tag conf m14 -fore [lindex $mergecolors 14] - $ctext tag conf m15 -fore [lindex $mergecolors 15] - $ctext tag conf mmax -fore darkgrey - set mergemax 16 - $ctext tag conf mresult -font textfontbold - $ctext tag conf msep -font textfontbold - $ctext tag conf found -back $foundbgcolor - $ctext tag conf currentsearchhit -back $currentsearchhitbgcolor - $ctext tag conf wwrap -wrap word -lmargin2 1c - $ctext tag conf bold -font textfontbold - - .pwbottom add .bleft - if {!$use_ttk} { - .pwbottom paneconfigure .bleft -width $geometry(botwidth) - } - - # lower right - ${NS}::frame .bright - ${NS}::frame .bright.mode - ${NS}::radiobutton .bright.mode.patch -text [mc "Patch"] \ - -command reselectline -variable cmitmode -value "patch" - ${NS}::radiobutton .bright.mode.tree -text [mc "Tree"] \ - -command reselectline -variable cmitmode -value "tree" - grid .bright.mode.patch .bright.mode.tree -sticky ew - pack .bright.mode -side top -fill x - set cflist .bright.cfiles - set indent [font measure mainfont "nn"] - text $cflist \ - -selectbackground $selectbgcolor \ - -background $bgcolor -foreground $fgcolor \ - -font mainfont \ - -tabs [list $indent [expr {2 * $indent}]] \ - -yscrollcommand ".bright.sb set" \ - -cursor [. cget -cursor] \ - -spacing1 1 -spacing3 1 - lappend bglist $cflist - lappend fglist $cflist - ${NS}::scrollbar .bright.sb -command "$cflist yview" - pack .bright.sb -side right -fill y - pack $cflist -side left -fill both -expand 1 - $cflist tag configure highlight \ - -background [$cflist cget -selectbackground] - $cflist tag configure bold -font mainfontbold - - .pwbottom add .bright - .ctop add .pwbottom - - # restore window width & height if known - if {[info exists geometry(main)]} { - if {[scan $geometry(main) "%dx%d" w h] >= 2} { - if {$w > [winfo screenwidth .]} { - set w [winfo screenwidth .] - } - if {$h > [winfo screenheight .]} { - set h [winfo screenheight .] - } - wm geometry . "${w}x$h" - } - } - - if {[info exists geometry(state)] && $geometry(state) eq "zoomed"} { - wm state . $geometry(state) - } - - if {[tk windowingsystem] eq {aqua}} { - set M1B M1 - set ::BM "3" - } else { - set M1B Control - set ::BM "2" - } - - if {$use_ttk} { - bind .ctop { - bind %W {} - %W sashpos 0 $::geometry(topheight) - } - bind .pwbottom { - bind %W {} - %W sashpos 0 $::geometry(botwidth) - } - } - - bind .pwbottom {resizecdetpanes %W %w} - pack .ctop -fill both -expand 1 - bindall <1> {selcanvline %W %x %y} - #bindall {selcanvline %W %x %y} - if {[tk windowingsystem] == "win32"} { - bind . { windows_mousewheel_redirector %W %X %Y %D } - bind $ctext { windows_mousewheel_redirector %W %X %Y %D ; break } - } else { - bindall "allcanvs yview scroll -5 units" - bindall "allcanvs yview scroll 5 units" - bind $ctext