From a4a001f671f01566a66b443ccf93cf8ea1803744 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 29 Aug 2024 11:50:28 +0200 Subject: [PATCH 01/84] reimplemenation --- Mergin/plugin.py | 7 + Mergin/project_history_dock.py | 183 +++++++++++++++++++++++++++ Mergin/ui/ui_project_history_dock.ui | 83 ++++++++++++ 3 files changed, 273 insertions(+) create mode 100644 Mergin/project_history_dock.py create mode 100644 Mergin/ui/ui_project_history_dock.ui diff --git a/Mergin/plugin.py b/Mergin/plugin.py index eea6f9ec..2f04619e 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -42,6 +42,7 @@ from .sync_dialog import SyncDialog from .configure_sync_wizard import DbSyncConfigWizard from .remove_project_dialog import RemoveProjectDialog +from .project_history_dock import ProjectHistoryDockWidget from .utils import ( ServerType, ClientError, @@ -183,6 +184,9 @@ def initGui(self): self.iface.addCustomActionForLayerType(self.action_export_mbtiles, "", QgsMapLayer.VectorTileLayer, False) self.action_export_mbtiles.triggered.connect(self.export_vector_tiles) + self.history_dock_widget = ProjectHistoryDockWidget(self.mc) + self.iface.addDockWidget(Qt.RightDockWidgetArea, self.history_dock_widget) + QgsProject.instance().layersAdded.connect(self.add_context_menu_actions) def add_action( @@ -537,6 +541,9 @@ def unload(self): self.iface.unregisterProjectPropertiesWidgetFactory(self.mergin_project_config_factory) + self.iface.removeDockWidget(self.history_dock_widget) + del self.history_dock_widget + remove_project_variables() QgsExpressionContextUtils.removeGlobalVariable("mergin_username") QgsExpressionContextUtils.removeGlobalVariable("mergin_url") diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py new file mode 100644 index 00000000..43b0398d --- /dev/null +++ b/Mergin/project_history_dock.py @@ -0,0 +1,183 @@ +import os +import math +import json +from urllib.error import URLError + +from PyQt5.QtCore import QObject +from qgis.PyQt import uic +from qgis.PyQt.QtGui import QIcon, QFont +from qgis.PyQt.QtCore import Qt, QThread, pyqtSignal, QSortFilterProxyModel, QAbstractItemModel, QModelIndex, QAbstractTableModel +from qgis.PyQt.QtWidgets import QMenu, QMessageBox + +from qgis.gui import QgsDockWidget +from qgis.core import Qgis, QgsMessageLog +from qgis.utils import iface + +from .diff_dialog import DiffViewerDialog +from .utils import ClientError, mergin_project_local_path + +from .mergin.merginproject import MerginProject +from .mergin.utils import int_version + +from .mergin import MerginClient + +class VersionsTableModel(QAbstractTableModel): + def __init__(self, parent=None): + super().__init__(parent) + self.versions = [] + + self.oldest = None + self.latest = None + + self.headers = ["Version", "Author", "Created"] + + def latest_version(self): + if len(self.versions) == 0: + return None + return int_version(self.versions[0]["name"]) + + def oldest_version(self): + if len(self.versions) == 0: + return None + return int_version(self.versions[-1]["name"]) + + + def rowCount(self, parent: QModelIndex): + return len(self.versions) + + def columnCount(self, parent: QModelIndex) -> int: + return len(self.headers) + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + return self.headers[section] + return None + + def data(self, index, role=Qt.DisplayRole): + if not index.isValid(): + return None + + idx = index.row() + if role == Qt.DisplayRole: + if index.column() == 0: + return self.versions[idx]["name"] + if index.column() == 1: + return self.versions[idx]["author"] + if index.column() == 2: + return "placeholder" + return contextual_date(self.versions[idx]["created"]) + else: + return None + + def insertRows(self, row, count, parent=QModelIndex()): + self.beginInsertRows(parent, row, row + count - 1) + self.endInsertRows() + + def append(self): + to_append = [{"name" : "azaz"},{"name" : "rezer"}] + + + self.insertRows(len(self.versions) - 1, len(to_append)) + self.versions.extend(to_append) + self.layoutChanged.emit() + + # iface.messageBar().pushMessage("len:", str(len(self.versions)), level=Qgis.Critical) + # for i in self.versions: + # # print(i) + # QgsMessageLog.logMessage("Error" + str(i), level=Qgis.Critical) + # # iface.messageBar().pushMessage("Error","fefe", level=Qgis.Critical) + + def add_versions(self, versions): + self.insertRows(len(self.versions) - 1, len(versions)) + self.versions.extend(versions) + self.layoutChanged.emit() + + def prepend(): + pass + + def canFetchMore(self, parent: QModelIndex) -> bool: + #Fetch while we are not the the first version + return self.oldest_version() == None or self.oldest_version() >= 1 + + def fetchMore(self, parent: QModelIndex) -> None: + pass + # fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) + # fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) + # fetcher.start() + + + + + + + + + +class VersionsFetcher(QThread): + + finished = pyqtSignal(list) + + def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel): + super(VersionsFetcher, self).__init__() + self.mc = mc + self.project_name = project_name + self.model = model + + self.per_page = 100 #server limit + + def run(self): + + if len(self.model.versions) == 0: + #initial fetch + info = self.mc.project_info(self.project_name) + to = int_version(info["version"]) + QgsMessageLog.logMessage("intit") + else: + to = self.model.latest_version() + + since = to - 100 + if since < 0: + since = 1 + versions = self.mc.project_versions(self.project_name, since=since, to=to) + versions.reverse() + + + + self.finished.emit(versions) + + + +ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_history_dock.ui") + +class ProjectHistoryDockWidget(QgsDockWidget): + def __init__(self, mc): + QgsDockWidget.__init__(self) + self.ui = uic.loadUi(ui_file, self) + + self.mc = mc + self.mp = MerginProject(mergin_project_local_path()) + + + self.model = VersionsTableModel() + # self.model.versions.extend([{"name" : "blabla"},{"name" : "blabla2"}]) + self.versions_tree.setModel(self.model) + + + self.fetch_from_server() + + + + self.view_changes_btn.clicked.connect(self.model.append) + + self.ui.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) + + def fetch_from_server(self): + + self.fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) + self.fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) + self.fetcher.start() + + def on_scrollbar_changed(self, value): + if self.ui.versions_tree.verticalScrollBar().maximum() <= value: + self.fetch_from_server() + diff --git a/Mergin/ui/ui_project_history_dock.ui b/Mergin/ui/ui_project_history_dock.ui new file mode 100644 index 00000000..10f775c5 --- /dev/null +++ b/Mergin/ui/ui_project_history_dock.ui @@ -0,0 +1,83 @@ + + + ProjectHistoryDockWidget + + + + 0 + 0 + 370 + 450 + + + + Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea + + + Mergin Maps history + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + + + + + Current project is not a Mergin project. Project history is not available. + + + true + + + + + + + + + + + Qt::CustomContextMenu + + + false + + + + + + + View changes + + + + + + + + + + + + + From 444ead8808c87a4cfaed3e3573d69a3327f9e370 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 29 Aug 2024 11:50:28 +0200 Subject: [PATCH 02/84] reimplemenation --- Mergin/plugin.py | 7 + Mergin/project_history_dock.py | 191 +++++++++++++++++++++++++++ Mergin/ui/ui_project_history_dock.ui | 83 ++++++++++++ 3 files changed, 281 insertions(+) create mode 100644 Mergin/project_history_dock.py create mode 100644 Mergin/ui/ui_project_history_dock.ui diff --git a/Mergin/plugin.py b/Mergin/plugin.py index eea6f9ec..2f04619e 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -42,6 +42,7 @@ from .sync_dialog import SyncDialog from .configure_sync_wizard import DbSyncConfigWizard from .remove_project_dialog import RemoveProjectDialog +from .project_history_dock import ProjectHistoryDockWidget from .utils import ( ServerType, ClientError, @@ -183,6 +184,9 @@ def initGui(self): self.iface.addCustomActionForLayerType(self.action_export_mbtiles, "", QgsMapLayer.VectorTileLayer, False) self.action_export_mbtiles.triggered.connect(self.export_vector_tiles) + self.history_dock_widget = ProjectHistoryDockWidget(self.mc) + self.iface.addDockWidget(Qt.RightDockWidgetArea, self.history_dock_widget) + QgsProject.instance().layersAdded.connect(self.add_context_menu_actions) def add_action( @@ -537,6 +541,9 @@ def unload(self): self.iface.unregisterProjectPropertiesWidgetFactory(self.mergin_project_config_factory) + self.iface.removeDockWidget(self.history_dock_widget) + del self.history_dock_widget + remove_project_variables() QgsExpressionContextUtils.removeGlobalVariable("mergin_username") QgsExpressionContextUtils.removeGlobalVariable("mergin_url") diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py new file mode 100644 index 00000000..2406c778 --- /dev/null +++ b/Mergin/project_history_dock.py @@ -0,0 +1,191 @@ +import os +import math +import json +from urllib.error import URLError + +from PyQt5.QtCore import QObject +from qgis.PyQt import uic +from qgis.PyQt.QtGui import QIcon, QFont +from qgis.PyQt.QtCore import Qt, QThread, pyqtSignal, QSortFilterProxyModel, QAbstractItemModel, QModelIndex, QAbstractTableModel +from qgis.PyQt.QtWidgets import QMenu, QMessageBox + +from qgis.gui import QgsDockWidget +from qgis.core import Qgis, QgsMessageLog +from qgis.utils import iface + +from .diff_dialog import DiffViewerDialog +from .utils import ClientError, mergin_project_local_path + +from .mergin.merginproject import MerginProject +from .mergin.utils import int_version + +from .mergin import MerginClient + +class VersionsTableModel(QAbstractTableModel): + def __init__(self, parent=None): + super().__init__(parent) + self.versions = [] + + self.oldest = None + self.latest = None + + self.headers = ["Version", "Author", "Created"] + + def latest_version(self): + if len(self.versions) == 0: + return None + return int_version(self.versions[0]["name"]) + + def oldest_version(self): + if len(self.versions) == 0: + return None + return int_version(self.versions[-1]["name"]) + + + def rowCount(self, parent: QModelIndex): + return len(self.versions) + + def columnCount(self, parent: QModelIndex) -> int: + return len(self.headers) + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + return self.headers[section] + return None + + def data(self, index, role=Qt.DisplayRole): + if not index.isValid(): + return None + + idx = index.row() + if role == Qt.DisplayRole: + if index.column() == 0: + return self.versions[idx]["name"] + if index.column() == 1: + return self.versions[idx]["author"] + if index.column() == 2: + return "placeholder" + return contextual_date(self.versions[idx]["created"]) + else: + return None + + def insertRows(self, row, count, parent=QModelIndex()): + self.beginInsertRows(parent, row, row + count - 1) + self.endInsertRows() + + def append(self): + to_append = [{"name" : "azaz"},{"name" : "rezer"}] + + + self.insertRows(len(self.versions) - 1, len(to_append)) + self.versions.extend(to_append) + self.layoutChanged.emit() + + # iface.messageBar().pushMessage("len:", str(len(self.versions)), level=Qgis.Critical) + # for i in self.versions: + # # print(i) + # QgsMessageLog.logMessage("Error" + str(i), level=Qgis.Critical) + # # iface.messageBar().pushMessage("Error","fefe", level=Qgis.Critical) + + def add_versions(self, versions): + self.insertRows(len(self.versions) - 1, len(versions)) + self.versions.extend(versions) + self.layoutChanged.emit() + + def prepend(): + pass + + def canFetchMore(self, parent: QModelIndex) -> bool: + #Fetch while we are not the the first version + return self.oldest_version() == None or self.oldest_version() >= 1 + + def fetchMore(self, parent: QModelIndex) -> None: + pass + # fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) + # fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) + # fetcher.start() + + + + + + + + + +class VersionsFetcher(QThread): + + finished = pyqtSignal(list) + + def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel): + super(VersionsFetcher, self).__init__() + self.mc = mc + self.project_name = project_name + self.model = model + + self.per_page = 100 #server limit + + def run(self): + + QgsMessageLog.logMessage("len: " + str(len(self.model.versions))) + + if len(self.model.versions) == 0: + #initial fetch + info = self.mc.project_info(self.project_name) + to = int_version(info["version"]) + QgsMessageLog.logMessage("intit") + else: + to = self.model.oldest_version() + since = to - 100 + if since < 0: + since = 1 + + versions = self.mc.project_versions(self.project_name, since=since, to=to) + versions.reverse() + + + + self.finished.emit(versions) + + + +ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_history_dock.ui") + +class ProjectHistoryDockWidget(QgsDockWidget): + def __init__(self, mc): + QgsDockWidget.__init__(self) + self.ui = uic.loadUi(ui_file, self) + + self.mc = mc + self.mp = MerginProject(mergin_project_local_path()) + + self.fetcher = None + + + self.model = VersionsTableModel() + # self.model.versions.extend([{"name" : "blabla"},{"name" : "blabla2"}]) + self.versions_tree.setModel(self.model) + + + self.fetch_from_server() + + + + self.view_changes_btn.clicked.connect(self.model.append) + + self.ui.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) + + def fetch_from_server(self): + + if self.fetcher and self.fetcher.isRunning(): + # Only fetching when previous is finshed + return + + self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.model) + self.fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) + self.fetcher.start() + + def on_scrollbar_changed(self, value): + if self.ui.versions_tree.verticalScrollBar().maximum() <= value: + self.fetch_from_server() + diff --git a/Mergin/ui/ui_project_history_dock.ui b/Mergin/ui/ui_project_history_dock.ui new file mode 100644 index 00000000..10f775c5 --- /dev/null +++ b/Mergin/ui/ui_project_history_dock.ui @@ -0,0 +1,83 @@ + + + ProjectHistoryDockWidget + + + + 0 + 0 + 370 + 450 + + + + Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea + + + Mergin Maps history + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + + + + + Current project is not a Mergin project. Project history is not available. + + + true + + + + + + + + + + + Qt::CustomContextMenu + + + false + + + + + + + View changes + + + + + + + + + + + + + From 0a89e33e1e1b4a8273b865f2ae56efd581cc5f60 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 4 Sep 2024 10:59:33 +0200 Subject: [PATCH 03/84] Bring back ui setup --- Mergin/project_history_dock.py | 63 ++++++++++++++++++++++++++++++---- Mergin/utils.py | 38 ++++++++++++++++++++ 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index 2406c778..34c8808b 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -14,7 +14,12 @@ from qgis.utils import iface from .diff_dialog import DiffViewerDialog -from .utils import ClientError, mergin_project_local_path +from .utils import ( + ClientError, + mergin_project_local_path, + check_mergin_subdirs, + contextual_date + ) from .mergin.merginproject import MerginProject from .mergin.utils import int_version @@ -31,6 +36,8 @@ def __init__(self, parent=None): self.headers = ["Version", "Author", "Created"] + self.current_version = None + def latest_version(self): if len(self.versions) == 0: return None @@ -64,8 +71,12 @@ def data(self, index, role=Qt.DisplayRole): if index.column() == 1: return self.versions[idx]["author"] if index.column() == 2: - return "placeholder" return contextual_date(self.versions[idx]["created"]) + elif role == Qt.FontRole: + if self.versions[idx]["name"] == self.current_version: + font = QFont() + font.setBold(True) + return font else: return None @@ -123,7 +134,7 @@ def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel): self.project_name = project_name self.model = model - self.per_page = 100 #server limit + self.per_page = 50 #server limit def run(self): @@ -157,8 +168,10 @@ def __init__(self, mc): self.ui = uic.loadUi(ui_file, self) self.mc = mc - self.mp = MerginProject(mergin_project_local_path()) + self.mp = None + self.project_path = None + self.fetcher = None @@ -166,14 +179,50 @@ def __init__(self, mc): # self.model.versions.extend([{"name" : "blabla"},{"name" : "blabla2"}]) self.versions_tree.setModel(self.model) + self.view_changes_btn.clicked.connect(self.model.append) - self.fetch_from_server() + self.ui.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) + if self.mc is None: + self.info_label.setText("Plugin is not configured.") + self.stackedWidget.setCurrentIndex(0) + return + + if self.project_path is None: + self.info_label.setText("Current project is not saved. Project history is not available.") + self.stackedWidget.setCurrentIndex(0) + return + + if not check_mergin_subdirs(self.project_path): + self.info_label.setText("Current project is not a Mergin project. Project history is not available.") + self.stackedWidget.setCurrentIndex(0) + return + + self.mp = MerginProject(self.project_path) + self.local_project_version = self.mp.version() + + try: + ws_id = self.mp.workspace_id() + except ClientError as e: + self.info_label.setText(str(e)) + self.stackedWidget.setCurrentIndex(0) + return + + # check if user has permissions + usage = self.mc.workspace_usage(ws_id) + if not usage["view_history"]["allowed"]: + self.info_label.setText("The workspace does not allow to view project history.") + self.stackedWidget.setCurrentIndex(0) + return + + self.stackedWidget.setCurrentIndex(1) + + + self.model.current_version = self.mp.version() + self.fetch_from_server() - self.view_changes_btn.clicked.connect(self.model.append) - self.ui.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) def fetch_from_server(self): diff --git a/Mergin/utils.py b/Mergin/utils.py index 603643f6..7f8b00fb 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1504,3 +1504,41 @@ def get_layer_by_path(path): safe_file_path = layer_path.split("|") if safe_file_path[0] == path: return layer + + +def check_mergin_subdirs(directory): + """Check if the directory has a Mergin Maps project subdir (.mergin).""" + for root, dirs, files in os.walk(directory): + for name in dirs: + if name == ".mergin": + return os.path.join(root, name) + return False + +def contextual_date(date_string, start_date=None): + """Converts datetime string returned by the server into contextual duration string, e.g. + 'N hours/days/month ago' + """ + dt = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%SZ") + now = datetime.now() if start_date is None else datetime.strptime(start_date, "%Y-%m-%dT%H:%M:%SZ") + delta = now - dt + if delta.days > 365: + years = now.year - dt.year - ((now.month, now.day) < (dt.month, dt.day)) + return f"{years} {'years' if years > 1 else 'year'} ago" + elif delta.days > 31: + months = int(delta.days // 30.436875) + return f"{months} {'months' if months > 1 else 'month'} ago" + elif delta.days > 6: + weeks = int(delta.days // 7) + return f"{weeks} {'weeks' if weeks > 1 else 'week'} ago" + + if delta.days < 1: + hours = delta.seconds // 3600 + if hours < 1: + minutes = (delta.seconds // 60) % 60 + if minutes <= 0: + return "just now" + return f"{minutes} {'minutes' if minutes > 1 else 'minute'} ago" + + return f"{hours} {'hours' if hours > 1 else 'hour'} ago" + + return f"{delta.days} {'days' if delta.days > 1 else 'day'} ago" From 17c31310b5ed39b39c5b6c2fad0b57492f1f4b3b Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 5 Sep 2024 11:58:23 +0200 Subject: [PATCH 04/84] switch to deque for storing model data --- Mergin/project_history_dock.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index 34c8808b..b2bf82f9 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -2,6 +2,7 @@ import math import json from urllib.error import URLError +from collections import deque from PyQt5.QtCore import QObject from qgis.PyQt import uic @@ -29,7 +30,9 @@ class VersionsTableModel(QAbstractTableModel): def __init__(self, parent=None): super().__init__(parent) - self.versions = [] + + #Keep ordered + self.versions = deque() self.oldest = None self.latest = None @@ -103,8 +106,10 @@ def add_versions(self, versions): self.versions.extend(versions) self.layoutChanged.emit() - def prepend(): - pass + def prepend_versions(self, versions): + self.insertRows(0, len(versions)) + self.versions.extendleft(versions) + self.layoutChanged.emit() def canFetchMore(self, parent: QModelIndex) -> bool: #Fetch while we are not the the first version @@ -112,6 +117,7 @@ def canFetchMore(self, parent: QModelIndex) -> bool: def fetchMore(self, parent: QModelIndex) -> None: pass + #emit # fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) # fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) # fetcher.start() @@ -170,7 +176,7 @@ def __init__(self, mc): self.mc = mc self.mp = None - self.project_path = None + self.project_path = mergin_project_local_path() self.fetcher = None From cc96ae733f0fea967341e4b7045c8329b7c44e4c Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 6 Sep 2024 10:17:16 +0200 Subject: [PATCH 05/84] Add clear method --- Mergin/project_history_dock.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index b2bf82f9..01076b31 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -86,6 +86,11 @@ def data(self, index, role=Qt.DisplayRole): def insertRows(self, row, count, parent=QModelIndex()): self.beginInsertRows(parent, row, row + count - 1) self.endInsertRows() + + def clear(self): + self.beginResetModel() + self.versions.clear() + self.endResetModel() def append(self): to_append = [{"name" : "azaz"},{"name" : "rezer"}] From 1934ba8c4fd5a553eb909ceb8e8077c8cc95232a Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 6 Sep 2024 10:19:48 +0200 Subject: [PATCH 06/84] Bring back toolbar button --- Mergin/images/default/tabler_icons/history.svg | 7 +++++++ Mergin/images/white/tabler_icons/history.svg | 7 +++++++ Mergin/plugin.py | 14 ++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 Mergin/images/default/tabler_icons/history.svg create mode 100644 Mergin/images/white/tabler_icons/history.svg diff --git a/Mergin/images/default/tabler_icons/history.svg b/Mergin/images/default/tabler_icons/history.svg new file mode 100644 index 00000000..2721ea7f --- /dev/null +++ b/Mergin/images/default/tabler_icons/history.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Mergin/images/white/tabler_icons/history.svg b/Mergin/images/white/tabler_icons/history.svg new file mode 100644 index 00000000..a72e8071 --- /dev/null +++ b/Mergin/images/white/tabler_icons/history.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Mergin/plugin.py b/Mergin/plugin.py index 2f04619e..6f78830a 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -47,6 +47,7 @@ ServerType, ClientError, LoginError, + InvalidProject, check_mergin_subdirs, create_mergin_client, find_qgis_files, @@ -156,6 +157,15 @@ def initGui(self): add_to_menu=True, add_to_toolbar=None, ) + self.history_dock_action = self.add_action( + "history.svg", + text="Project History", + callback=self.toggle_project_history_dock, + add_to_menu=False, + add_to_toolbar=self.toolbar, + enabled=False, + always_on=False, + ) self.enable_toolbar_actions() @@ -328,6 +338,10 @@ def configure_db_sync(self): if not wizard.exec_(): return + + def toggle_project_history_dock(self): + self.history_dock_widget.toggleUserVisible() + def show_no_workspaces_dialog(self): msg = ( "Workspace is a place to store your projects and share them with your colleagues. " From 3dc9919c7cde2a7cd37199be54b55abd71aa6002 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 6 Sep 2024 12:08:21 +0200 Subject: [PATCH 07/84] Add new icon --- Mergin/images/default/tabler_icons/file-description.svg | 9 +++++++++ Mergin/images/white/tabler_icons/file-description.svg | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 Mergin/images/default/tabler_icons/file-description.svg create mode 100644 Mergin/images/white/tabler_icons/file-description.svg diff --git a/Mergin/images/default/tabler_icons/file-description.svg b/Mergin/images/default/tabler_icons/file-description.svg new file mode 100644 index 00000000..74d7b5dc --- /dev/null +++ b/Mergin/images/default/tabler_icons/file-description.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Mergin/images/white/tabler_icons/file-description.svg b/Mergin/images/white/tabler_icons/file-description.svg new file mode 100644 index 00000000..80d71103 --- /dev/null +++ b/Mergin/images/white/tabler_icons/file-description.svg @@ -0,0 +1,9 @@ + + + + + + + + + From 07e3ae040c5af8df574c3a1a1c70854bf974bfd4 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 10 Sep 2024 11:09:10 +0200 Subject: [PATCH 08/84] Bring back version detail --- Mergin/project_history_dock.py | 129 ++++++++++++++++++++-- Mergin/ui/ui_version_details_dialog.ui | 147 +++++++++++++++++++++++++ Mergin/utils.py | 5 + Mergin/version_details_dialog.py | 78 +++++++++++++ 4 files changed, 351 insertions(+), 8 deletions(-) create mode 100644 Mergin/ui/ui_version_details_dialog.ui create mode 100644 Mergin/version_details_dialog.py diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index 01076b31..e57bb5ee 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -15,11 +15,14 @@ from qgis.utils import iface from .diff_dialog import DiffViewerDialog +from .version_details_dialog import VersionDetailsDialog + from .utils import ( ClientError, mergin_project_local_path, check_mergin_subdirs, - contextual_date + contextual_date, + icon_path, ) from .mergin.merginproject import MerginProject @@ -126,13 +129,59 @@ def fetchMore(self, parent: QModelIndex) -> None: # fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) # fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) # fetcher.start() + + def item_from_index(self, index): + return self.versions[index.row()] + - - - + +class ChangesetsDownloader(QThread): + """ + Class to download version changesets in background worker thread + """ + + finished = pyqtSignal(str) + + def __init__(self, mc, mp, version): + """ + ChangesetsDownloader constructor + + :param mc: MerginClient instance + :param mp: MerginProject instance + :param version: project version to download + """ + super(ChangesetsDownloader, self).__init__() + self.mc = mc + self.mp = mp + self.version = version + + def run(self): + info = self.mc.project_info(self.mp.project_full_name(), version=f"v{self.version}") + files = [f for f in info["files"] if is_versioned_file(f["path"])] + if not files: + self.finished.emit("This version does not contain changes in the project layers.") + return + + has_history = any("diff" in f for f in files) + if not has_history: + self.finished.emit("This version does not contain changes in the project layers.") + return + + for f in files: + if self.isInterruptionRequested(): + return + + if "diff" not in f: + continue + file_diffs = self.mc.download_file_diffs(self.mp.dir, f["path"], [f"v{self.version}"]) + full_gpkg = self.mp.fpath_cache(f["path"], version=f"v{self.version}") + if not os.path.exists(full_gpkg): + self.mc.download_file(self.mp.dir, f["path"], full_gpkg, f"v{self.version}") + + self.finished.emit("") class VersionsFetcher(QThread): @@ -184,15 +233,21 @@ def __init__(self, mc): self.project_path = mergin_project_local_path() self.fetcher = None + self.diff_downloader = None + self.model = VersionsTableModel() # self.model.versions.extend([{"name" : "blabla"},{"name" : "blabla2"}]) self.versions_tree.setModel(self.model) + self.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) + + QgsMessageLog.logMessage("attach") - self.view_changes_btn.clicked.connect(self.model.append) + self.versions_tree.customContextMenuRequested.connect(self.show_context_menu) + QgsMessageLog.logMessage("end Attach") - self.ui.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) + self.view_changes_btn.clicked.connect(self.model.append) if self.mc is None: @@ -233,8 +288,6 @@ def __init__(self, mc): self.model.current_version = self.mp.version() self.fetch_from_server() - - def fetch_from_server(self): if self.fetcher and self.fetcher.isRunning(): @@ -249,3 +302,63 @@ def on_scrollbar_changed(self, value): if self.ui.versions_tree.verticalScrollBar().maximum() <= value: self.fetch_from_server() + + + def show_context_menu(self, pos): + """Shows context menu in the project history dock""" + index = self.versions_tree.indexAt(pos) + if not index.isValid(): + return + + item = self.model.item_from_index(index) + version_name = item["name"] + version = int_version(item["name"]) + QgsMessageLog.logMessage("Open meu sdz" + str(item)) + + menu = QMenu() + view_details_action = menu.addAction("Version details") + view_details_action.setIcon(QIcon(icon_path("file-description.svg"))) + view_details_action.triggered.connect(lambda: self.version_details(version_name)) + view_changes_action = menu.addAction("View changes") + view_changes_action.setIcon(QIcon(icon_path("file-diff.svg"))) + view_changes_action.triggered.connect(lambda: self.view_changes(version)) + + + menu.exec_(self.versions_tree.mapToGlobal(pos)) + + + + def version_details(self, version): + """Shows version information with full view of added/updated/removed files""" + + data = self.mc.project_version_info(self.mp.project_id(), version) + dlg = VersionDetailsDialog(data) + dlg.exec_() + + def view_changes(self, version): + """Initiates download of changesets for the given version if they are not present + in the cache. Otherwise use cached changesets to show diff viewer dialog. + """ + if not os.path.exists(os.path.join(self.project_path, ".mergin", ".cache", f"v{version}")): + if self.diff_downloader and self.diff_downloader.isRunning(): + self.diff_downloader.requestInterruption() + + self.diff_downloader = ChangesetsDownloader(self.mc, self.mp, version) + self.diff_downloader.finished.connect(lambda msg: self.show_diff_viewer(version, msg)) + self.diff_downloader.start() + else: + self.show_diff_viewer(version) + + def show_diff_viewer(self, version, msg=""): + """Shows a Diff Viewer dialog with changes made in the specific version. + If msg is not empty string, show message box and returns. + """ + if msg != "": + QMessageBox.information(None, "Mergin", msg) + return + + dlg = DiffViewerDialog(version) + if dlg.diff_layers: + dlg.exec_() + else: + QMessageBox.information(None, "Mergin", "No changes to the current project layers for this version.") \ No newline at end of file diff --git a/Mergin/ui/ui_version_details_dialog.ui b/Mergin/ui/ui_version_details_dialog.ui new file mode 100644 index 00000000..864907b0 --- /dev/null +++ b/Mergin/ui/ui_version_details_dialog.ui @@ -0,0 +1,147 @@ + + + Dialog + + + + 0 + 0 + 401 + 335 + + + + Version Details + + + + + + Version + + + + + + + true + + + + + + + Author + + + + + + + true + + + + + + + Project size + + + + + + + true + + + + + + + Created + + + + + + + true + + + + + + + User Agent + + + + + + + true + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/Mergin/utils.py b/Mergin/utils.py index 7f8b00fb..037f1071 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1542,3 +1542,8 @@ def contextual_date(date_string, start_date=None): return f"{hours} {'hours' if hours > 1 else 'hour'} ago" return f"{delta.days} {'days' if delta.days > 1 else 'day'} ago" + +def format_datetime(date_string): + """Formats datetime string returned by the server into human-readable format""" + dt = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%SZ") + return dt.strftime("%a, %d %b %Y %H:%M:%S GMT") \ No newline at end of file diff --git a/Mergin/version_details_dialog.py b/Mergin/version_details_dialog.py new file mode 100644 index 00000000..6def050d --- /dev/null +++ b/Mergin/version_details_dialog.py @@ -0,0 +1,78 @@ +import os + +from qgis.PyQt import uic +from qgis.PyQt.QtWidgets import QDialog +from qgis.PyQt.QtGui import QStandardItemModel, QStandardItem, QIcon + +from qgis.gui import QgsGui +from qgis.core import QgsMessageLog + +from .utils import is_versioned_file, icon_path, format_datetime + +from .mergin.utils import bytes_to_human_size + +ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_version_details_dialog.ui") + + +class VersionDetailsDialog(QDialog): + icons = { + "added": "plus.svg", + "removed": "trash.svg", + "updated": "pencil.svg", + "renamed": "pencil.svg", + "table": "table.svg", + } + + def __init__(self, version_details, parent=None): + QDialog.__init__(self, parent) + self.ui = uic.loadUi(ui_file, self) + QgsGui.instance().enableAutoGeometryRestore(self) + + self.version_details = version_details + + self.model = QStandardItemModel() + self.model.setHorizontalHeaderLabels(["Details"]) + self.tree_details.setModel(self.model) + self.populate_details() + self.tree_details.expandAll() + + def populate_details(self): + self.edit_version.setText(self.version_details["name"]) + self.edit_author.setText(self.version_details["author"]) + self.edit_project_size.setText(bytes_to_human_size(self.version_details["project_size"])) + self.edit_created.setText(format_datetime(self.version_details["created"])) + self.edit_user_agent.setText(self.version_details["user_agent"]) + + root_item = QStandardItem(f"Changes in version {self.version_details['name']}") + self.model.appendRow(root_item) + for category in self.version_details["changes"]: + for item in self.version_details["changes"][category]: + path = item["path"] + item = self._get_icon_item(category, path) + if is_versioned_file(path): + if path in self.version_details["changesets"]: + for sub_item in self._versioned_file_summary_items( + self.version_details["changesets"][path]["summary"] + ): + item.appendRow(sub_item) + # QgsMessageLog.logMessage(str("changes") + "/" + str(category) + "/" + sub_item) + root_item.appendRow(item) + + def _get_icon_item(self, key, text): + path = icon_path(self.icons[key]) + item = QStandardItem(text) + item.setIcon(QIcon(path)) + return item + + def _versioned_file_summary_items(self, summary): + items = [] + for s in summary: + table_name_item = self._get_icon_item("table", s["table"]) + for row in self._table_summary_items(s): + table_name_item.appendRow(row) + items.append(table_name_item) + + return items + + def _table_summary_items(self, summary): + return [QStandardItem("{}: {}".format(k, summary[k])) for k in summary if k != "table"] From 1ab9eaff21322870099762271d40f48d14484ed8 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 10 Sep 2024 13:34:25 +0200 Subject: [PATCH 09/84] brnig back update history on synchronise --- Mergin/plugin.py | 1 + Mergin/project_history_dock.py | 43 +++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Mergin/plugin.py b/Mergin/plugin.py index 6f78830a..b0bfe7fe 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -477,6 +477,7 @@ def create_new_project(self): def current_project_sync(self): """Synchronise current Mergin Maps project.""" self.manager.project_status(self.mergin_proj_dir) + self.history_dock_widget.fetch_sync_server() def find_project(self): """Open new Find Mergin Maps project dialog""" diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index e57bb5ee..3d5d0c0b 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -188,16 +188,27 @@ class VersionsFetcher(QThread): finished = pyqtSignal(list) - def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel): + def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel, is_sync=False): super(VersionsFetcher, self).__init__() self.mc = mc self.project_name = project_name self.model = model + self.is_sync = is_sync + self.per_page = 50 #server limit def run(self): + if (not self.is_sync): + versions = self.fetch_previous() + else: + versions = self.fetch_sync_history() + + self.finished.emit(versions) + + def fetch_previous(self): + QgsMessageLog.logMessage("len: " + str(len(self.model.versions))) if len(self.model.versions) == 0: @@ -214,10 +225,25 @@ def run(self): versions = self.mc.project_versions(self.project_name, since=since, to=to) versions.reverse() + return versions + def fetch_sync_history(self): + + QgsMessageLog.logMessage("len: " + str(len(self.model.versions))) - self.finished.emit(versions) - + #deter latest + info = self.mc.project_info(self.project_name) + + latest_server = int_version(info["version"]) + to = self.model.latest_version() + + versions = self.mc.project_versions(self.project_name, since=latest_server, to=to) + versions.pop() #Remove the last as we already have it + versions.reverse() + + QgsMessageLog.logMessage(str(versions)) + + return versions ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_history_dock.ui") @@ -297,6 +323,17 @@ def fetch_from_server(self): self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.model) self.fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) self.fetcher.start() + + def fetch_sync_server(self): + + if self.fetcher and self.fetcher.isRunning(): + # Only fetching when previous is finshed + self.fetcher.requestInterruption() + + self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.model, is_sync=True) + self.fetcher.finished.connect(lambda versions: self.model.prepend_versions(versions)) + self.fetcher.start() + def on_scrollbar_changed(self, value): if self.ui.versions_tree.verticalScrollBar().maximum() <= value: From fc56e808bbf503b25408a399ab735c24c5218605 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 11 Sep 2024 10:17:34 +0200 Subject: [PATCH 10/84] Fix update on changing project and configuration --- Mergin/plugin.py | 9 +++++++++ Mergin/project_history_dock.py | 20 +++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Mergin/plugin.py b/Mergin/plugin.py index b0bfe7fe..a2114d6d 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -88,12 +88,15 @@ def __init__(self, iface): self.current_workspace = dict() self.provider = MerginProvider() + self.history_dock_widget = None + if self.iface is not None: self.toolbar = self.iface.addToolBar("Mergin Maps Toolbar") self.toolbar.setToolTip("Mergin Maps Toolbar") self.toolbar.setObjectName("MerginMapsToolbar") self.iface.projectRead.connect(self.on_qgis_project_changed) + self.on_qgis_project_changed() self.iface.newProjectCreated.connect(self.on_qgis_project_changed) settings = QSettings() @@ -196,6 +199,8 @@ def initGui(self): self.history_dock_widget = ProjectHistoryDockWidget(self.mc) self.iface.addDockWidget(Qt.RightDockWidgetArea, self.history_dock_widget) + self.history_dock_widget.hide() + QgsProject.instance().layersAdded.connect(self.add_context_menu_actions) @@ -311,6 +316,8 @@ def configure(self): self.mc = dlg.writeSettings() self.on_config_changed() self.show_browser_panel() + self.history_dock_widget.set_mergin_client(self.mc) + def configure_db_sync(self): """Open db-sync setup wizard.""" @@ -532,6 +539,8 @@ def on_qgis_project_changed(self): self.mergin_proj_dir = mergin_project_local_path() if self.mergin_proj_dir is not None: self.enable_toolbar_actions() + if self.history_dock_widget: + self.history_dock_widget.on_qgis_project_changed() def add_context_menu_actions(self, layers): provider_names = "vectortile" diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index 3d5d0c0b..154a41ac 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -237,6 +237,9 @@ def fetch_sync_history(self): latest_server = int_version(info["version"]) to = self.model.latest_version() + QgsMessageLog.logMessage("from: " + str(latest_server)) + QgsMessageLog.logMessage("to: " + str(to)) + versions = self.mc.project_versions(self.project_name, since=latest_server, to=to) versions.pop() #Remove the last as we already have it versions.reverse() @@ -275,7 +278,10 @@ def __init__(self, mc): self.view_changes_btn.clicked.connect(self.model.append) + self.update_ui() + + def update_ui(self): if self.mc is None: self.info_label.setText("Plugin is not configured.") self.stackedWidget.setCurrentIndex(0) @@ -313,7 +319,7 @@ def __init__(self, mc): self.model.current_version = self.mp.version() self.fetch_from_server() - + def fetch_from_server(self): if self.fetcher and self.fetcher.isRunning(): @@ -358,7 +364,7 @@ def show_context_menu(self, pos): view_details_action.triggered.connect(lambda: self.version_details(version_name)) view_changes_action = menu.addAction("View changes") view_changes_action.setIcon(QIcon(icon_path("file-diff.svg"))) - view_changes_action.triggered.connect(lambda: self.view_changes(version)) + view_changes_action.triggered.connect(lambda: self.view_changes(version_name)) menu.exec_(self.versions_tree.mapToGlobal(pos)) @@ -398,4 +404,12 @@ def show_diff_viewer(self, version, msg=""): if dlg.diff_layers: dlg.exec_() else: - QMessageBox.information(None, "Mergin", "No changes to the current project layers for this version.") \ No newline at end of file + QMessageBox.information(None, "Mergin", "No changes to the current project layers for this version.") + + def set_mergin_client(self, mc): + self.mc = mc + + def on_qgis_project_changed(self): + self.model.clear() + self.project_path = mergin_project_local_path() + self.update_ui() \ No newline at end of file From af6984de6a7529ef487b3dcac1fa11cdd2f8597e Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 11 Sep 2024 12:02:37 +0200 Subject: [PATCH 11/84] Bring back view version --- Mergin/diff.py | 80 ++++++++++++++++++++++++++++++++++ Mergin/diff_dialog.py | 19 +++++++- Mergin/project_history_dock.py | 4 +- 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/Mergin/diff.py b/Mergin/diff.py index 5f33b1d6..84db9b7b 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -3,6 +3,7 @@ import base64 import sqlite3 import tempfile +import glob from qgis.PyQt.QtCore import QVariant @@ -106,6 +107,16 @@ def parse_db_schema(db_file): tables[tbl["table"]] = TableSchema(tbl["table"], columns) return tables +def db_schema_from_json(schema_json): + """Create map of tables from the schema JSON file""" + tables = {} # key: name, value: TableSchema + for tbl in schema_json: + columns = [] + for col in tbl["columns"]: + columns.append(ColumnSchema(col["name"], col["type"], "primary_key" in col and col["primary_key"])) + + tables[tbl["table"]] = TableSchema(tbl["table"], columns) + return tables def parse_diff(geodiff, diff_file): """ @@ -311,6 +322,75 @@ def make_local_changes_layer(mp, layer): return vl, "" +def make_version_changes_layers(project_path, version): + geodiff = pygeodiff.GeoDiff() + + layers = [] + version_dir = os.path.join(project_path, ".mergin", ".cache", f"v{version}") + for f in glob.iglob("*.gpkg", root_dir=version_dir): + gpkg_file = os.path.join(version_dir, f) + schema_file = gpkg_file + "-schema.json" + if not os.path.exists(schema_file): + geodiff.schema("sqlite", "", gpkg_file, schema_file) + + changeset_file = find_changeset_file(f, version_dir) + if changeset_file is None: + continue + + with open(schema_file, encoding="utf-8") as fl: + data = fl.read() + schema_json = json.loads(data.replace("\n", "")).get("geodiff_schema") + + db_schema = db_schema_from_json(schema_json) + diff = parse_diff(geodiff, changeset_file) + + for table_name in diff.keys(): + fields, cols_to_fields = create_field_list(db_schema[table_name]) + geom_type, geom_crs = get_layer_geometry_info(schema_json, table_name) + + db_conn = None # no ref. db + db_conn = sqlite3.connect(gpkg_file) + + features = diff_table_to_features(diff[table_name], db_schema[table_name], fields, cols_to_fields, db_conn) + + # create diff layer + if geom_type is None: + continue + + uri = f"{geom_type}?crs=epsg:{geom_crs}" if geom_crs else geom_type + vl = QgsVectorLayer(uri, f"[v{version} changes] {table_name}", "memory") + if not vl.isValid(): + continue + + vl.dataProvider().addAttributes(fields) + vl.updateFields() + vl.dataProvider().addFeatures(features) + + style_diff_layer(vl, db_schema[table_name]) + layers.append(vl) + + return layers + +def find_changeset_file(file_name, version_dir): + """Returns path to the diff file for the given version file""" + for f in glob.iglob("*.gpkg-diff*", root_dir=version_dir): + if f.startswith(file_name): + return os.path.join(version_dir, f) + return None + + +def get_layer_geometry_info(schema_json, table_name): + """Returns geometry type and CRS for a given table""" + for tbl in schema_json: + if tbl["table"] == table_name: + for col in tbl["columns"]: + if col["type"] == "geometry": + return col["geometry"]["type"], col["geometry"]["srs_id"] + return "NoGeometry", "" + + return None, None + + def style_diff_layer(layer, schema_table): """Apply conditional styling and symbology to diff layer""" ### setup conditional styles! diff --git a/Mergin/diff_dialog.py b/Mergin/diff_dialog.py index 654ca19a..1746a2a1 100644 --- a/Mergin/diff_dialog.py +++ b/Mergin/diff_dialog.py @@ -18,14 +18,14 @@ from qgis.utils import iface, OverrideCursor from .mergin.merginproject import MerginProject -from .diff import make_local_changes_layer +from .diff import make_local_changes_layer, make_version_changes_layers from .utils import icon_path ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_diff_viewer_dialog.ui") class DiffViewerDialog(QDialog): - def __init__(self, parent=None): + def __init__(self, version=None, parent=None): QDialog.__init__(self, parent) self.ui = uic.loadUi(ui_file, self) @@ -91,6 +91,8 @@ def __init__(self, parent=None): self.diff_layers = [] self.filter_model = None + self.version = version + self.create_tabs() def reject(self): @@ -106,6 +108,12 @@ def save_splitter_state(self): settings.setValue("Mergin/changesViewerSplitterSize", self.splitter.saveState()) def create_tabs(self): + if self.version is None: + self.show_local_changes() + else: + self.show_version_changes() + + def show_local_changes(self): mp = MerginProject(QgsProject.instance().homePath()) project_layers = QgsProject.instance().mapLayers() for layer in project_layers.values(): @@ -125,6 +133,13 @@ def create_tabs(self): self.tab_bar.addTab(self.icon_for_layer(vl), f"{layer.name()} ({vl.featureCount()})") self.tab_bar.setCurrentIndex(0) + def show_version_changes(self): + layers = make_version_changes_layers(QgsProject.instance().homePath(), self.version) + for vl in layers: + self.diff_layers.append(vl) + self.tab_bar.addTab(self.icon_for_layer(vl), f"{vl.name()} ({vl.featureCount()})") + self.tab_bar.setCurrentIndex(0) + def toggle_project_layers(self, checked): layers = self.collect_layers(checked) self.update_canvas(layers) diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index 154a41ac..30949395 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -26,7 +26,7 @@ ) from .mergin.merginproject import MerginProject -from .mergin.utils import int_version +from .mergin.utils import int_version, is_versioned_file from .mergin import MerginClient @@ -364,7 +364,7 @@ def show_context_menu(self, pos): view_details_action.triggered.connect(lambda: self.version_details(version_name)) view_changes_action = menu.addAction("View changes") view_changes_action.setIcon(QIcon(icon_path("file-diff.svg"))) - view_changes_action.triggered.connect(lambda: self.view_changes(version_name)) + view_changes_action.triggered.connect(lambda: self.view_changes(version)) menu.exec_(self.versions_tree.mapToGlobal(pos)) From 9b6afc7eec4d56eeacb438caabc09c5851da8481 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 11 Sep 2024 14:07:54 +0200 Subject: [PATCH 12/84] Remove superfluous log --- Mergin/project_history_dock.py | 26 +++----------------------- Mergin/version_details_dialog.py | 1 - 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index 30949395..bc6286a1 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -102,12 +102,6 @@ def append(self): self.insertRows(len(self.versions) - 1, len(to_append)) self.versions.extend(to_append) self.layoutChanged.emit() - - # iface.messageBar().pushMessage("len:", str(len(self.versions)), level=Qgis.Critical) - # for i in self.versions: - # # print(i) - # QgsMessageLog.logMessage("Error" + str(i), level=Qgis.Critical) - # # iface.messageBar().pushMessage("Error","fefe", level=Qgis.Critical) def add_versions(self, versions): self.insertRows(len(self.versions) - 1, len(versions)) @@ -209,13 +203,10 @@ def run(self): def fetch_previous(self): - QgsMessageLog.logMessage("len: " + str(len(self.model.versions))) - if len(self.model.versions) == 0: #initial fetch info = self.mc.project_info(self.project_name) to = int_version(info["version"]) - QgsMessageLog.logMessage("intit") else: to = self.model.oldest_version() since = to - 100 @@ -229,23 +220,16 @@ def fetch_previous(self): def fetch_sync_history(self): - QgsMessageLog.logMessage("len: " + str(len(self.model.versions))) - - #deter latest + #determine latest info = self.mc.project_info(self.project_name) latest_server = int_version(info["version"]) - to = self.model.latest_version() + since = self.model.latest_version() - QgsMessageLog.logMessage("from: " + str(latest_server)) - QgsMessageLog.logMessage("to: " + str(to)) - - versions = self.mc.project_versions(self.project_name, since=latest_server, to=to) + versions = self.mc.project_versions(self.project_name, since=since, to=latest_server) versions.pop() #Remove the last as we already have it versions.reverse() - QgsMessageLog.logMessage(str(versions)) - return versions @@ -271,10 +255,7 @@ def __init__(self, mc): self.versions_tree.setModel(self.model) self.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) - QgsMessageLog.logMessage("attach") - self.versions_tree.customContextMenuRequested.connect(self.show_context_menu) - QgsMessageLog.logMessage("end Attach") self.view_changes_btn.clicked.connect(self.model.append) @@ -356,7 +337,6 @@ def show_context_menu(self, pos): item = self.model.item_from_index(index) version_name = item["name"] version = int_version(item["name"]) - QgsMessageLog.logMessage("Open meu sdz" + str(item)) menu = QMenu() view_details_action = menu.addAction("Version details") diff --git a/Mergin/version_details_dialog.py b/Mergin/version_details_dialog.py index 6def050d..730a8210 100644 --- a/Mergin/version_details_dialog.py +++ b/Mergin/version_details_dialog.py @@ -55,7 +55,6 @@ def populate_details(self): self.version_details["changesets"][path]["summary"] ): item.appendRow(sub_item) - # QgsMessageLog.logMessage(str("changes") + "/" + str(category) + "/" + sub_item) root_item.appendRow(item) def _get_icon_item(self, key, text): From 8890df54ffb4683ff02e16aa27c8d53de9df9820 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 17 Sep 2024 12:32:12 +0200 Subject: [PATCH 13/84] cleanup useless function --- Mergin/project_history_dock.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index bc6286a1..5602c953 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -94,14 +94,6 @@ def clear(self): self.beginResetModel() self.versions.clear() self.endResetModel() - - def append(self): - to_append = [{"name" : "azaz"},{"name" : "rezer"}] - - - self.insertRows(len(self.versions) - 1, len(to_append)) - self.versions.extend(to_append) - self.layoutChanged.emit() def add_versions(self, versions): self.insertRows(len(self.versions) - 1, len(versions)) @@ -127,10 +119,6 @@ def fetchMore(self, parent: QModelIndex) -> None: def item_from_index(self, index): return self.versions[index.row()] - - - - class ChangesetsDownloader(QThread): """ From e1685ee61d66d5b728efcaef46f8b6693e1c05b8 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 23 Sep 2024 09:46:48 +0200 Subject: [PATCH 14/84] Proof of concept version viewer --- Mergin/project_history_dock.py | 8 +- Mergin/ui/ui_draft_versions_viewer.ui | 372 ++++++++++++++++++++++++++ Mergin/version_viewer_dialog.py | 250 +++++++++++++++++ 3 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 Mergin/ui/ui_draft_versions_viewer.ui create mode 100644 Mergin/version_viewer_dialog.py diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index 5602c953..7cf289dc 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -16,6 +16,7 @@ from .diff_dialog import DiffViewerDialog from .version_details_dialog import VersionDetailsDialog +from .version_viewer_dialog import VersionViewerDialog from .utils import ( ClientError, @@ -245,10 +246,15 @@ def __init__(self, mc): self.versions_tree.customContextMenuRequested.connect(self.show_context_menu) - self.view_changes_btn.clicked.connect(self.model.append) + self.view_changes_btn.clicked.connect(self.show_version_viewer) self.update_ui() + def show_version_viewer(self): + dlg = VersionViewerDialog() + dlg.exec_() + + def update_ui(self): if self.mc is None: diff --git a/Mergin/ui/ui_draft_versions_viewer.ui b/Mergin/ui/ui_draft_versions_viewer.ui new file mode 100644 index 00000000..b5bab3c3 --- /dev/null +++ b/Mergin/ui/ui_draft_versions_viewer.ui @@ -0,0 +1,372 @@ + + + Dialog + + + + 0 + 0 + 1295 + 736 + + + + Changes Viewer + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 24 + 24 + + + + Navigate to first feature + + + + + + + :/images/themes/default/mActionDoubleArrowLeft.svg:/images/themes/default/mActionDoubleArrowLeft.svg + + + true + + + + + + + + 24 + 24 + + + + Navigate to previous feature + + + + + + + :/images/themes/default/mActionArrowLeft.svg:/images/themes/default/mActionArrowLeft.svg + + + true + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + Navigate to next feature + + + + + + + :/images/themes/default/mActionArrowRight.svg:/images/themes/default/mActionArrowRight.svg + + + true + + + + + + + + 11 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 20 + 20 + + + + + + + + + + + Automatically zoom to the current feature + + + + + + + :/images/themes/default/mActionZoomTo.svg:/images/themes/default/mActionZoomTo.svg + + + true + + + true + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + false + + + + + + + 1 + + + + + + + Qt::Vertical + + + + + + + + + + + + 110 + 300 + 231 + 81 + + + + + 13 + + + + No visual changes + + + + + + + + + + + + + + + Wed, 11 Sep 2024 10:36:41 GMT + + + true + + + + + + + User Agent + + + + + + + Input/2024.3.1 (android/13.0) + + + true + + + + + + + Project size + + + + + + + 4.3 MB + + + true + + + + + + + Created + + + + + + + + + false + + + QTabWidget::South + + + QTabWidget::Rounded + + + 0 + + + false + + + false + + + + Layers + + + + + + + + + + Files + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + QgsMapCanvas + QGraphicsView +
qgis.gui
+ 1 +
+ + QgsAttributeTableView + QTableView +
qgsattributetableview.h
+
+
+ + +
diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py new file mode 100644 index 00000000..6582ebbd --- /dev/null +++ b/Mergin/version_viewer_dialog.py @@ -0,0 +1,250 @@ +import os + +from qgis.PyQt import uic, QtCore +from qgis.PyQt.QtWidgets import QDialog, QAction +from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon +from qgis.PyQt.QtCore import QStringListModel, Qt +from qgis.utils import iface +from qgis.core import ( + QgsMessageLog, + QgsApplication, + QgsFeatureRequest, + QgsVectorLayerCache +) +from qgis.gui import QgsGui, QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel + + +from .utils import is_versioned_file, icon_path, format_datetime +from .mergin.utils import bytes_to_human_size + +ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_draft_versions_viewer.ui") + + +class VersionViewerDialog(QDialog): + def __init__(self, parent=None): + + QDialog.__init__(self, parent) + self.ui = uic.loadUi(ui_file, self) + + self.history_model = QStandardItemModel() + + # self.history_model.setHeaderData(0, Qt.Horizontal, "Versions", Qt.DisplayRole) + self.history_model.setHorizontalHeaderItem(0, QStandardItem("Versions")) + + self.history_treeview.setModel(self.history_model) + + root_node = self.history_model.invisibleRootItem() + date_1 = QStandardItem("September 16") + + item = QStandardItem("v25 ValentinB") + + date_1.appendRow(QStandardItem("v25 ValentinB")) + date_1.appendRow(QStandardItem("v24 ValentinB")) + date_1.appendRow(QStandardItem("v23 ValentinB")) + + + date_2 = QStandardItem("September 17") + date_2.appendRow(QStandardItem("v22 ValentinB")) + date_2.appendRow(QStandardItem("v21 ValentinB")) + + root_node.appendRow(date_1) + root_node.appendRow(date_2) + + self.history_treeview.expandRecursively(root_node.index()) + + model = QStringListModel() + model.setStringList(["Line example", "New_scratch_layer"]) + self.layer_list.setModel(model) + + height = 30 + self.toolbar.setMinimumHeight(height) + self.temporal_control.setMinimumHeight(height) + # self.verticalSpacer_3.setMinimumHeight(100) + self.verticalLayout.insertSpacing(0,height) + + self.map_canvas.setDisabled(False) + self.map_canvas.setEnabled(False) + + # self.toolbar. + self.zoom_full_action = QAction( + QgsApplication.getThemeIcon("/mActionZoomFullExtent.svg"), "Zoom Full", self + ) + self.toolbar.addAction(self.zoom_full_action) + + self.zoom_selected_action = QAction( + QgsApplication.getThemeIcon("/mActionZoomToSelected.svg"), "Zoom To Selection", self + ) + self.toolbar.addAction(self.zoom_selected_action) + + height = max( + [self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()] + ) + self.splitter.setSizes([height, height]) + + + layers = iface.mapCanvas().layers() + self.map_canvas.setLayers(layers) + + self.layer_cache = QgsVectorLayerCache(layers[0], 1000) + self.layer_cache.setCacheGeometry(False) + + self.table_model = QgsAttributeTableModel(self.layer_cache) + self.table_model.setRequest(QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setLimit(100)) + + self.filter_model = QgsAttributeTableFilterModel(self.map_canvas, self.table_model) + + self.attribute_table.setModel(self.filter_model) + self.table_model.loadLayer() + + self.map_canvas.setDestinationCrs(layers[0].crs()) + extent = layers[0].extent() + d = min(extent.width(), extent.height()) + if d == 0: + d = 1 + extent = extent.buffered(d * 0.07) + self.map_canvas.setExtent(extent) + + + + # self.history_verticalLayout.hide() + self.splitter_2.setSizes([50,50, 200]) + self.splitter_2.setCollapsible(0, True) + + # self.tabWidget.tabBar().setDocumentMode(True) + # self.tabWidget.tabBar().setExpanding(False) + + # self.splitter_2.setStretchFactor(0, 0) + + # self.attribute_table_2.hide() + + self.version_details = { + "author": "ValentinB_lutraconsulting", + "changes": { + "added": [], + "removed": [], + "updated": [ + { + "change": "updated", + "checksum": "5c9cdf250e6fbfe37c55dbb6dc299ce5db697136", + "diff": { + "checksum": "d42ac00c1ac157bab94e0627d104d49767c98d63", + "path": "line example.gpkg-diff-d58ebf64-15d7-4847-840d-2dec44a6c987", + "size": 129 + }, + "expiration": "2024-09-08T14:35:04Z", + "mtime": "2024-09-06T14:35:07Z", + "path": "line example.gpkg", + "size": 98304 + }, + { + "change": "updated", + "checksum": "3e833120cc04d3bc88a3c38bb14b58dea3e3e9d2", + "diff": { + "checksum": "7b86f60f3665982c03e39a1c3a186ff64fba8439", + "path": "New_scratch_layer.gpkg-diff-01ffb906-086b-4980-b205-6c50284e8ac5", + "size": 291 + }, + "expiration": "2024-09-08T14:35:07Z", + "mtime": "2024-09-06T14:35:07Z", + "path": "New_scratch_layer.gpkg", + "size": 98304 + } + ] + }, + "changesets": { + "New_scratch_layer.gpkg": { + "size": 291, + "summary": [ + { + "delete": 1, + "insert": 1, + "table": "New_scratch_layer", + "update": 0 + } + ] + }, + "line example.gpkg": { + "size": 129, + "summary": [ + { + "delete": 0, + "insert": 1, + "table": "line example", + "update": 0 + } + ] + } + }, + "created": "2024-09-06T14:35:07Z", + "name": "v21", + "namespace": "solar panel workspace cop", + "project_name": "terrain_poi2", + "project_size": 4533043, + "user_agent": "Input/2024.3.1 (android/13.0)" + } + + self.icons = { + "added": "plus.svg", + "removed": "trash.svg", + "updated": "pencil.svg", + "renamed": "pencil.svg", + "table": "table.svg", + } + self.model = QStandardItemModel() + self.model.setHorizontalHeaderLabels(["Details"]) + self.details_treeview.setModel(self.model) + self.populate_details() + self.details_treeview.expandAll() + + def populate_details(self): + self.edit_project_size.setText(bytes_to_human_size(self.version_details["project_size"])) + self.edit_created.setText(format_datetime(self.version_details["created"])) + self.edit_user_agent.setText(self.version_details["user_agent"]) + + root_item = QStandardItem(f"Changes in version {self.version_details['name']}") + self.model.appendRow(root_item) + for category in self.version_details["changes"]: + for item in self.version_details["changes"][category]: + path = item["path"] + item = self._get_icon_item(category, path) + if is_versioned_file(path): + if path in self.version_details["changesets"]: + for sub_item in self._versioned_file_summary_items( + self.version_details["changesets"][path]["summary"] + ): + item.appendRow(sub_item) + root_item.appendRow(item) + + def _get_icon_item(self, key, text): + path = icon_path(self.icons[key]) + item = QStandardItem(text) + item.setIcon(QIcon(path)) + return item + + def _versioned_file_summary_items(self, summary): + items = [] + for s in summary: + table_name_item = self._get_icon_item("table", s["table"]) + for row in self._table_summary_items(s): + table_name_item.appendRow(row) + items.append(table_name_item) + + return items + + def _table_summary_items(self, summary): + return [QStandardItem("{}: {}".format(k, summary[k])) for k in summary if k != "table"] + + + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_C: + # QgsMessageLog.logMessage("blabla") + if self.stackedWidget.currentIndex() == 0: + self.stackedWidget.setCurrentIndex(1) + self.tabWidget.setCurrentIndex(1) + else: + self.stackedWidget.setCurrentIndex(0) + self.tabWidget.setCurrentIndex(0) + + return + From bf49863b962d55168e477cd8dcab63d9363f8a64 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 30 Sep 2024 16:31:51 +0200 Subject: [PATCH 15/84] Update version viewer --- Mergin/diff.py | 17 +- Mergin/diff_dialog.py | 19 +- Mergin/plugin.py | 10 +- Mergin/project_history_dock.py | 5 +- Mergin/ui/ui_draft_versions_viewer.ui | 9 +- Mergin/version_viewer_dialog.py | 551 ++++++++++++++++++++------ 6 files changed, 464 insertions(+), 147 deletions(-) diff --git a/Mergin/diff.py b/Mergin/diff.py index 84db9b7b..8d207a5f 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -7,9 +7,10 @@ from qgis.PyQt.QtCore import QVariant -from qgis.PyQt.QtGui import QColor +from qgis.PyQt.QtGui import QColor, QIcon from qgis.core import ( + QgsApplication, QgsVectorLayer, QgsFeature, QgsGeometry, @@ -526,3 +527,17 @@ def style_diff_layer(layer, schema_table): ) r = QgsRuleBasedRenderer(root_rule) layer.setRenderer(r) + + +def icon_for_layer(layer) -> QIcon: + geom_type = layer.geometryType() + if geom_type == QgsWkbTypes.PointGeometry: + return QgsApplication.getThemeIcon("/mIconPointLayer.svg") + elif geom_type == QgsWkbTypes.LineGeometry: + return QgsApplication.getThemeIcon("/mIconLineLayer.svg") + elif geom_type == QgsWkbTypes.PolygonGeometry: + return QgsApplication.getThemeIcon("/mIconPolygonLayer.svg") + elif geom_type == QgsWkbTypes.UnknownGeometry: + return QgsApplication.getThemeIcon("/mIconGeometryCollectionLayer.svg") + else: + return QgsApplication.getThemeIcon("/mIconTableLayer.svg") diff --git a/Mergin/diff_dialog.py b/Mergin/diff_dialog.py index 1746a2a1..ca571c65 100644 --- a/Mergin/diff_dialog.py +++ b/Mergin/diff_dialog.py @@ -18,7 +18,7 @@ from qgis.utils import iface, OverrideCursor from .mergin.merginproject import MerginProject -from .diff import make_local_changes_layer, make_version_changes_layers +from .diff import make_local_changes_layer, make_version_changes_layers, icon_for_layer from .utils import icon_path ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_diff_viewer_dialog.ui") @@ -130,14 +130,14 @@ def show_local_changes(self): continue self.diff_layers.append(vl) - self.tab_bar.addTab(self.icon_for_layer(vl), f"{layer.name()} ({vl.featureCount()})") + self.tab_bar.addTab(icon_for_layer(vl), f"{layer.name()} ({vl.featureCount()})") self.tab_bar.setCurrentIndex(0) def show_version_changes(self): layers = make_version_changes_layers(QgsProject.instance().homePath(), self.version) for vl in layers: self.diff_layers.append(vl) - self.tab_bar.addTab(self.icon_for_layer(vl), f"{vl.name()} ({vl.featureCount()})") + self.tab_bar.addTab(icon_for_layer(vl), f"{vl.name()} ({vl.featureCount()})") self.tab_bar.setCurrentIndex(0) def toggle_project_layers(self, checked): @@ -214,18 +214,7 @@ def zoom_selected(self): self.map_canvas.zoomToSelected([self.current_diff]) self.map_canvas.refresh() - def icon_for_layer(self, layer): - geom_type = layer.geometryType() - if geom_type == QgsWkbTypes.PointGeometry: - return QgsApplication.getThemeIcon("/mIconPointLayer.svg") - elif geom_type == QgsWkbTypes.LineGeometry: - return QgsApplication.getThemeIcon("/mIconLineLayer.svg") - elif geom_type == QgsWkbTypes.PolygonGeometry: - return QgsApplication.getThemeIcon("/mIconPolygonLayer.svg") - elif geom_type == QgsWkbTypes.UnknownGeometry: - return QgsApplication.getThemeIcon("/mIconGeometryCollectionLayer.svg") - else: - return QgsApplication.getThemeIcon("/mIconTableLayer.svg") + def show_unsaved_changes_warning(self): self.ui.messageBar.pushMessage( diff --git a/Mergin/plugin.py b/Mergin/plugin.py index dc9f4e66..50eb3876 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -43,6 +43,7 @@ from .configure_sync_wizard import DbSyncConfigWizard from .remove_project_dialog import RemoveProjectDialog from .project_history_dock import ProjectHistoryDockWidget +from .version_viewer_dialog import VersionViewerDialog from .utils import ( ServerType, ClientError, @@ -64,7 +65,7 @@ unsaved_project_check, UnsavedChangesStrategy, ) - +from .mergin.utils import int_version, is_versioned_file from .mergin.merginproject import MerginProject from .processing.provider import MerginProvider import processing @@ -163,7 +164,7 @@ def initGui(self): self.history_dock_action = self.add_action( "history.svg", text="Project History", - callback=self.toggle_project_history_dock, + callback=self.open_project_history_window, add_to_menu=False, add_to_toolbar=self.toolbar, enabled=False, @@ -348,7 +349,10 @@ def configure_db_sync(self): wizard = DbSyncConfigWizard(project_name) if not wizard.exec_(): return - + + def open_project_history_window(self): + dlg = VersionViewerDialog(self.mc) + dlg.exec_() def toggle_project_history_dock(self): self.history_dock_widget.toggleUserVisible() diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index 7cf289dc..cadf8470 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -7,7 +7,7 @@ from PyQt5.QtCore import QObject from qgis.PyQt import uic from qgis.PyQt.QtGui import QIcon, QFont -from qgis.PyQt.QtCore import Qt, QThread, pyqtSignal, QSortFilterProxyModel, QAbstractItemModel, QModelIndex, QAbstractTableModel +from qgis.PyQt.QtCore import Qt, QThread, pyqtSignal, QModelIndex, QAbstractTableModel from qgis.PyQt.QtWidgets import QMenu, QMessageBox from qgis.gui import QgsDockWidget @@ -240,7 +240,6 @@ def __init__(self, mc): self.model = VersionsTableModel() - # self.model.versions.extend([{"name" : "blabla"},{"name" : "blabla2"}]) self.versions_tree.setModel(self.model) self.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) @@ -341,6 +340,8 @@ def show_context_menu(self, pos): view_changes_action.triggered.connect(lambda: self.view_changes(version)) + + menu.exec_(self.versions_tree.mapToGlobal(pos)) diff --git a/Mergin/ui/ui_draft_versions_viewer.ui b/Mergin/ui/ui_draft_versions_viewer.ui index b5bab3c3..f02c01d9 100644 --- a/Mergin/ui/ui_draft_versions_viewer.ui +++ b/Mergin/ui/ui_draft_versions_viewer.ui @@ -22,7 +22,7 @@ - + 0 @@ -203,7 +203,7 @@ - 1 + 0 @@ -325,7 +325,7 @@ - + @@ -342,6 +342,9 @@ 0 + + true + diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 6582ebbd..cd20a519 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -1,11 +1,21 @@ +from collections import deque import os from qgis.PyQt import uic, QtCore -from qgis.PyQt.QtWidgets import QDialog, QAction -from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon -from qgis.PyQt.QtCore import QStringListModel, Qt +from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem +from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont +from qgis.PyQt.QtCore import ( + QStringListModel, + Qt, + QModelIndex, + QAbstractTableModel, + QThread, + pyqtSignal + ) + from qgis.utils import iface from qgis.core import ( + QgsProject, QgsMessageLog, QgsApplication, QgsFeatureRequest, @@ -14,58 +24,298 @@ from qgis.gui import QgsGui, QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel -from .utils import is_versioned_file, icon_path, format_datetime -from .mergin.utils import bytes_to_human_size +from .utils import ( + ServerType, + ClientError, + LoginError, + InvalidProject, + check_mergin_subdirs, + create_mergin_client, + find_qgis_files, + get_mergin_auth, + icon_path, + mm_symbol_path, + is_number, + login_error_message, + mergin_project_local_path, + PROJS_PER_PAGE, + remove_project_variables, + same_dir, + unhandled_exception_message, + unsaved_project_check, + UnsavedChangesStrategy, + contextual_date, + is_versioned_file, + icon_path, + format_datetime +) + +from .mergin.merginproject import MerginProject +from .mergin.utils import bytes_to_human_size, int_version + +from .mergin import MerginClient +from .diff import make_version_changes_layers, icon_for_layer + ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_draft_versions_viewer.ui") -class VersionViewerDialog(QDialog): +class VersionsTableModel(QAbstractTableModel): + VERSION = Qt.UserRole + 1 + VERSION_NAME = Qt.UserRole + 2 + def __init__(self, parent=None): + super().__init__(parent) + + #Keep ordered + self.versions = deque() + + self.oldest = None + self.latest = None + + self.headers = ["Version", "Author", "Created"] + + self.current_version = None + + def latest_version(self): + if len(self.versions) == 0: + return None + return int_version(self.versions[0]["name"]) + + def oldest_version(self): + if len(self.versions) == 0: + return None + return int_version(self.versions[-1]["name"]) + + + def rowCount(self, parent: QModelIndex): + return len(self.versions) + + def columnCount(self, parent: QModelIndex) -> int: + return len(self.headers) + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + return self.headers[section] + return None + + def data(self, index, role=Qt.DisplayRole): + if not index.isValid(): + return None + + idx = index.row() + if role == Qt.DisplayRole: + if index.column() == 0: + return self.versions[idx]["name"] + if index.column() == 1: + return self.versions[idx]["author"] + if index.column() == 2: + return contextual_date(self.versions[idx]["created"]) + elif role == Qt.FontRole: + if self.versions[idx]["name"] == self.current_version: + font = QFont() + font.setBold(True) + return font + elif role == Qt.ToolTipRole: + if index.column() == 2: + return format_datetime(self.versions[idx]["created"]) + elif role == VersionsTableModel.VERSION: + return int_version(self.versions[idx]["name"]) + elif role == VersionsTableModel.VERSION_NAME: + return self.versions[idx]["name"] + else: + return None + + def insertRows(self, row, count, parent=QModelIndex()): + self.beginInsertRows(parent, row, row + count - 1) + self.endInsertRows() + + def clear(self): + self.beginResetModel() + self.versions.clear() + self.endResetModel() + + def add_versions(self, versions): + self.insertRows(len(self.versions) - 1, len(versions)) + self.versions.extend(versions) + self.layoutChanged.emit() + + def prepend_versions(self, versions): + self.insertRows(0, len(versions)) + self.versions.extendleft(versions) + self.layoutChanged.emit() + + def canFetchMore(self, parent: QModelIndex) -> bool: + #Fetch while we are not the the first version + return self.oldest_version() == None or self.oldest_version() >= 1 + + def fetchMore(self, parent: QModelIndex) -> None: + pass + #emit + # fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) + # fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) + # fetcher.start() + + def item_from_index(self, index): + return self.versions[index.row()] + + + +class ChangesetsDownloader(QThread): + """ + Class to download version changesets in background worker thread + """ + + finished = pyqtSignal(str) + + def __init__(self, mc, mp, version): + """ + ChangesetsDownloader constructor + + :param mc: MerginClient instance + :param mp: MerginProject instance + :param version: project version to download + """ + super(ChangesetsDownloader, self).__init__() + self.mc = mc + self.mp = mp + self.version = version + + def run(self): + info = self.mc.project_info(self.mp.project_full_name(), version=f"v{self.version}") + files = [f for f in info["files"] if is_versioned_file(f["path"])] + if not files: + self.finished.emit("This version does not contain changes in the project layers.") + return + + has_history = any("diff" in f for f in files) + if not has_history: + self.finished.emit("This version does not contain changes in the project layers.") + return + + for f in files: + if self.isInterruptionRequested(): + return + + if "diff" not in f: + continue + file_diffs = self.mc.download_file_diffs(self.mp.dir, f["path"], [f"v{self.version}"]) + full_gpkg = self.mp.fpath_cache(f["path"], version=f"v{self.version}") + if not os.path.exists(full_gpkg): + self.mc.download_file(self.mp.dir, f["path"], full_gpkg, f"v{self.version}") + + self.finished.emit("") + + +class VersionsFetcher(QThread): + + finished = pyqtSignal(list) + + def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel, is_sync=False): + super(VersionsFetcher, self).__init__() + self.mc = mc + self.project_name = project_name + self.model = model + + self.is_sync = is_sync + + self.per_page = 50 #server limit + + def run(self): + + if (not self.is_sync): + versions = self.fetch_previous() + else: + versions = self.fetch_sync_history() + + self.finished.emit(versions) + + def fetch_previous(self): + + if len(self.model.versions) == 0: + #initial fetch + info = self.mc.project_info(self.project_name) + to = int_version(info["version"]) + else: + to = self.model.oldest_version() + since = to - 100 + if since < 0: + since = 1 + + versions = self.mc.project_versions(self.project_name, since=since, to=to) + versions.reverse() + + return versions + + def fetch_sync_history(self): + + #determine latest + info = self.mc.project_info(self.project_name) + + latest_server = int_version(info["version"]) + since = self.model.latest_version() + + versions = self.mc.project_versions(self.project_name, since=since, to=latest_server) + versions.pop() #Remove the last as we already have it + versions.reverse() + + return versions + + +class VersionViewerDialog(QDialog): + def __init__(self,mc, parent=None): QDialog.__init__(self, parent) self.ui = uic.loadUi(ui_file, self) - self.history_model = QStandardItemModel() - # self.history_model.setHeaderData(0, Qt.Horizontal, "Versions", Qt.DisplayRole) - self.history_model.setHorizontalHeaderItem(0, QStandardItem("Versions")) + self.mc = mc + self.mp = None - self.history_treeview.setModel(self.history_model) - - root_node = self.history_model.invisibleRootItem() - date_1 = QStandardItem("September 16") + self.project_path = mergin_project_local_path() + self.mp = MerginProject(self.project_path) - item = QStandardItem("v25 ValentinB") - date_1.appendRow(QStandardItem("v25 ValentinB")) - date_1.appendRow(QStandardItem("v24 ValentinB")) - date_1.appendRow(QStandardItem("v23 ValentinB")) + self.fetcher = None + self.diff_downloader = None + self.model = VersionsTableModel() + self.history_treeview.setModel(self.model) - date_2 = QStandardItem("September 17") - date_2.appendRow(QStandardItem("v22 ValentinB")) - date_2.appendRow(QStandardItem("v21 ValentinB")) + # self.list_model = QStringListModel() + # self.list_model.setStringList(["Line example\n 2 add 2 removed", "New_scratch_layer"]) + + # self.layer_list.setModel(self.list_model) - root_node.appendRow(date_1) - root_node.appendRow(date_2) + + self.history_treeview.clicked.connect(self.handle_click) + # self.history_treeview.currentChanged.connect(lambda index, _previous : self.handle_click(index)) + self.fetch_from_server() - self.history_treeview.expandRecursively(root_node.index()) - model = QStringListModel() - model.setStringList(["Line example", "New_scratch_layer"]) - self.layer_list.setModel(model) height = 30 self.toolbar.setMinimumHeight(height) - self.temporal_control.setMinimumHeight(height) + + self.history_control.setMinimumHeight(height) + self.history_control.setVisible(False) # self.verticalSpacer_3.setMinimumHeight(100) self.verticalLayout.insertSpacing(0,height) - self.map_canvas.setDisabled(False) - self.map_canvas.setEnabled(False) + # self.map_canvas.setDisabled(False) + # self.map_canvas.setEnabled(False) + + + self.toggle_layers_action = QAction( + QgsApplication.getThemeIcon("/mActionAddLayer.svg"), "Toggle Project Layers", self + ) + self.toggle_layers_action.setCheckable(True) + self.toggle_layers_action.setChecked(True) + self.toggle_layers_action.toggled.connect(self.toggle_project_layers) + self.toolbar.addAction(self.toggle_layers_action) + + self.toolbar.addSeparator() - # self.toolbar. self.zoom_full_action = QAction( QgsApplication.getThemeIcon("/mActionZoomFullExtent.svg"), "Zoom Full", self ) @@ -82,29 +332,6 @@ def __init__(self, parent=None): self.splitter.setSizes([height, height]) - layers = iface.mapCanvas().layers() - self.map_canvas.setLayers(layers) - - self.layer_cache = QgsVectorLayerCache(layers[0], 1000) - self.layer_cache.setCacheGeometry(False) - - self.table_model = QgsAttributeTableModel(self.layer_cache) - self.table_model.setRequest(QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setLimit(100)) - - self.filter_model = QgsAttributeTableFilterModel(self.map_canvas, self.table_model) - - self.attribute_table.setModel(self.filter_model) - self.table_model.loadLayer() - - self.map_canvas.setDestinationCrs(layers[0].crs()) - extent = layers[0].extent() - d = min(extent.width(), extent.height()) - if d == 0: - d = 1 - extent = extent.buffered(d * 0.07) - self.map_canvas.setExtent(extent) - - # self.history_verticalLayout.hide() self.splitter_2.setSizes([50,50, 200]) @@ -117,71 +344,23 @@ def __init__(self, parent=None): # self.attribute_table_2.hide() - self.version_details = { - "author": "ValentinB_lutraconsulting", - "changes": { - "added": [], - "removed": [], - "updated": [ - { - "change": "updated", - "checksum": "5c9cdf250e6fbfe37c55dbb6dc299ce5db697136", - "diff": { - "checksum": "d42ac00c1ac157bab94e0627d104d49767c98d63", - "path": "line example.gpkg-diff-d58ebf64-15d7-4847-840d-2dec44a6c987", - "size": 129 - }, - "expiration": "2024-09-08T14:35:04Z", - "mtime": "2024-09-06T14:35:07Z", - "path": "line example.gpkg", - "size": 98304 - }, - { - "change": "updated", - "checksum": "3e833120cc04d3bc88a3c38bb14b58dea3e3e9d2", - "diff": { - "checksum": "7b86f60f3665982c03e39a1c3a186ff64fba8439", - "path": "New_scratch_layer.gpkg-diff-01ffb906-086b-4980-b205-6c50284e8ac5", - "size": 291 - }, - "expiration": "2024-09-08T14:35:07Z", - "mtime": "2024-09-06T14:35:07Z", - "path": "New_scratch_layer.gpkg", - "size": 98304 - } - ] - }, - "changesets": { - "New_scratch_layer.gpkg": { - "size": 291, - "summary": [ - { - "delete": 1, - "insert": 1, - "table": "New_scratch_layer", - "update": 0 - } - ] - }, - "line example.gpkg": { - "size": 129, - "summary": [ - { - "delete": 0, - "insert": 1, - "table": "line example", - "update": 0 - } - ] - } - }, - "created": "2024-09-06T14:35:07Z", - "name": "v21", - "namespace": "solar panel workspace cop", - "project_name": "terrain_poi2", - "project_size": 4533043, - "user_agent": "Input/2024.3.1 (android/13.0)" - } + self.current_diff = None + self.diff_layers = [] + self.filter_model = None + + + + self.layer_list.currentRowChanged.connect(self.diff_layer_changed) + + self.show_version_changes(25) + self.update_canvas(self.diff_layers) + self.diff_layer_changed(1) + + + + + self.version_details = self.mc.project_version_info(self.mp.project_id(), "v25") + self.icons = { "added": "plus.svg", @@ -190,19 +369,57 @@ def __init__(self, parent=None): "renamed": "pencil.svg", "table": "table.svg", } - self.model = QStandardItemModel() - self.model.setHorizontalHeaderLabels(["Details"]) - self.details_treeview.setModel(self.model) + self.model_detail = QStandardItemModel() + self.model_detail.setHorizontalHeaderLabels(["Details"]) + self.details_treeview.setModel(self.model_detail) + self.populate_details() + self.details_treeview.expandAll() + + self.model.current_version = self.mp.version() + self.fetch_from_server() + + + def handle_click(self, index: QModelIndex): + item = self.model.item_from_index(index) + version_name = item["name"] + version = int_version(item["name"]) + + self.version_details = self.mc.project_version_info(self.mp.project_id(), version_name) self.populate_details() self.details_treeview.expandAll() + # Reset layer list + QgsMessageLog.logMessage("Rest list") + # self.list_model.setStringList([]) + # self.list_model.removeRows( 0, self.list_model.rowCount() ) + self.layer_list.clear() + + self.show_version_changes(version) + self.update_canvas(self.diff_layers) + + return self.model.data(index, VersionsTableModel.VERSION) + + def set_mergin_client(self, mc): + self.mc = mc + + def fetch_from_server(self): + + if self.fetcher and self.fetcher.isRunning(): + # Only fetching when previous is finshed + return + + self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.model) + self.fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) + self.fetcher.start() + def populate_details(self): self.edit_project_size.setText(bytes_to_human_size(self.version_details["project_size"])) self.edit_created.setText(format_datetime(self.version_details["created"])) self.edit_user_agent.setText(self.version_details["user_agent"]) + self.model_detail.clear() root_item = QStandardItem(f"Changes in version {self.version_details['name']}") - self.model.appendRow(root_item) + self.model_detail.appendRow(root_item) for category in self.version_details["changes"]: for item in self.version_details["changes"][category]: path = item["path"] @@ -234,11 +451,99 @@ def _versioned_file_summary_items(self, summary): def _table_summary_items(self, summary): return [QStandardItem("{}: {}".format(k, summary[k])) for k in summary if k != "table"] + def toggle_project_layers(self, checked): + layers = self.collect_layers(checked) + self.update_canvas(layers) + + def update_canvas(self, layers): + self.map_canvas.setLayers(layers) + if layers: + self.map_canvas.setDestinationCrs(layers[0].crs()) + extent = layers[0].extent() + d = min(extent.width(), extent.height()) + if d == 0: + d = 1 + extent = extent.buffered(d * 0.07) + self.map_canvas.setExtent(extent) + self.map_canvas.refresh() + + + def show_version_changes(self, version): + self.diff_layers.clear() + + + layers = make_version_changes_layers(QgsProject.instance().homePath(), version) + for vl in layers: + self.diff_layers.append(vl) + QgsMessageLog.logMessage(f"{vl.name()}") + icon = icon_for_layer(vl) + + self.layer_list.addItem(QListWidgetItem(icon, f"{vl.name()} ({vl.featureCount()})\n 2 updated")) + # self.tab_bar.addTab(self.icon_for_layer(vl), f"{vl.name()} ({vl.featureCount()})") + + + if len(self.diff_layers) >= 1: + QgsMessageLog.logMessage("Visual change") + self.toolbar.setEnabled(True) + self.layer_list.setCurrentRow(0) + self.stackedWidget.setCurrentIndex(0) + self.tabWidget.setCurrentIndex(0) + else: + QgsMessageLog.logMessage("no Visual change") + self.toolbar.setEnabled(False) + self.stackedWidget.setCurrentIndex(1) + self.tabWidget.setCurrentIndex(1) + # self.list_model.setStringList([vl.name() for vl in self.diff_layers]) + # self.tab_bar.setCurrentIndex(0) + + + def collect_layers(self, checked): + if checked: + layers = iface.mapCanvas().layers() + else: + layers = [] + + if self.current_diff: + layers.insert(0, self.current_diff) + + return layers + + def diff_layer_changed(self, index): + if index > len(self.diff_layers): + return + + self.map_canvas.setLayers([]) + self.attribute_table.clearSelection() + + self.current_diff = self.diff_layers[index] + + self.layer_cache = QgsVectorLayerCache(self.current_diff, 1000) + self.layer_cache.setCacheGeometry(False) + + self.table_model = QgsAttributeTableModel(self.layer_cache) + self.table_model.setRequest(QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setLimit(100)) + + self.filter_model = QgsAttributeTableFilterModel(self.map_canvas, self.table_model) + + self.layer_cache.setParent(self.table_model) + + self.attribute_table.setModel(self.filter_model) + self.table_model.loadLayer() + + config = self.current_diff.attributeTableConfig() + self.filter_model.setAttributeTableConfig(config) + self.attribute_table.setAttributeTableConfig(config) + + layers = self.collect_layers(self.toggle_layers_action.isChecked()) + # layers = self.collect_layers(True) + self.update_canvas(layers) def keyPressEvent(self, event): + QgsMessageLog.logMessage("eventxezdede") + if event.key() == QtCore.Qt.Key_C: - # QgsMessageLog.logMessage("blabla") + QgsMessageLog.logMessage("blabla" + str(self.stackedWidget.currentIndex())) if self.stackedWidget.currentIndex() == 0: self.stackedWidget.setCurrentIndex(1) self.tabWidget.setCurrentIndex(1) From 0e18db5fa6bfbce0b622670fc7b0b5d0dcafa1b9 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 4 Oct 2024 02:21:40 +0200 Subject: [PATCH 16/84] bring back missing actions --- Mergin/ui/ui_draft_versions_viewer.ui | 2 +- Mergin/version_viewer_dialog.py | 38 +++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/Mergin/ui/ui_draft_versions_viewer.ui b/Mergin/ui/ui_draft_versions_viewer.ui index f02c01d9..5ae806ba 100644 --- a/Mergin/ui/ui_draft_versions_viewer.ui +++ b/Mergin/ui/ui_draft_versions_viewer.ui @@ -15,7 +15,7 @@ - + Qt::Horizontal diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index cd20a519..b4e84aa3 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -2,7 +2,7 @@ import os from qgis.PyQt import uic, QtCore -from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem +from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem, QPushButton, QMenu from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont from qgis.PyQt.QtCore import ( QStringListModel, @@ -326,21 +326,42 @@ def __init__(self,mc, parent=None): ) self.toolbar.addAction(self.zoom_selected_action) + + btn_add_changes = QPushButton("Add to project") + btn_add_changes.setIcon(QgsApplication.getThemeIcon("/mActionAdd.svg")) + menu = QMenu() + add_current_action = menu.addAction( + QIcon(icon_path("file-plus.svg")), "Add current changes layer to project" + ) + add_current_action.triggered.connect(self.add_current_to_project) + add_all_action = menu.addAction(QIcon(icon_path("folder-plus.svg")), "Add all changes layers to project") + add_all_action.triggered.connect(self.add_all_to_project) + btn_add_changes.setMenu(menu) + + self.toolbar.addWidget(btn_add_changes) + self.toolbar.setIconSize(iface.iconSize()) + height = max( [self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()] ) self.splitter.setSizes([height, height]) + + + + + + # self.history_verticalLayout.hide() - self.splitter_2.setSizes([50,50, 200]) - self.splitter_2.setCollapsible(0, True) + self.splitter_vertical.setSizes([120,200, 40]) + # self.splitter_vertical.setCollapsible(0, True) # self.tabWidget.tabBar().setDocumentMode(True) # self.tabWidget.tabBar().setExpanding(False) - # self.splitter_2.setStretchFactor(0, 0) + # self.splitter_vertical.setStretchFactor(0, 0) # self.attribute_table_2.hide() @@ -538,7 +559,14 @@ def diff_layer_changed(self, index): # layers = self.collect_layers(True) self.update_canvas(layers) - + def add_current_to_project(self): + if self.current_diff: + QgsProject.instance().addMapLayer(self.current_diff) + + def add_all_to_project(self): + for layer in self.diff_layers: + QgsProject.instance().addMapLayer(layer) + def keyPressEvent(self, event): QgsMessageLog.logMessage("eventxezdede") From c2bdff38b97681425a4271463083350d236d71e1 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 4 Oct 2024 02:51:21 +0200 Subject: [PATCH 17/84] splitter behaviour --- Mergin/version_viewer_dialog.py | 36 ++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index b4e84aa3..bfd818f6 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -7,6 +7,7 @@ from qgis.PyQt.QtCore import ( QStringListModel, Qt, + QSettings, QModelIndex, QAbstractTableModel, QThread, @@ -341,12 +342,9 @@ def __init__(self,mc, parent=None): self.toolbar.addWidget(btn_add_changes) self.toolbar.setIconSize(iface.iconSize()) - height = max( - [self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()] - ) - self.splitter.setSizes([height, height]) + self.set_splitters_state() @@ -355,7 +353,6 @@ def __init__(self,mc, parent=None): # self.history_verticalLayout.hide() - self.splitter_vertical.setSizes([120,200, 40]) # self.splitter_vertical.setCollapsible(0, True) # self.tabWidget.tabBar().setDocumentMode(True) @@ -420,6 +417,35 @@ def handle_click(self, index: QModelIndex): return self.model.data(index, VersionsTableModel.VERSION) + def closeEvent(self, event): + self.save_splitters_state() + QDialog.closeEvent(self, event) + + def save_splitters_state(self): + settings = QSettings() + settings.setValue("Mergin/VersionViewerSplitterSize", self.splitter.saveState()) + settings.setValue("Mergin/VersionViewerSplitterVericalSize", self.splitter_vertical.saveState()) + + def set_splitters_state(self): + settings = QSettings() + state_vertical = settings.value("Mergin/VersionViewerSplitterVericalSize") + if state_vertical: + self.splitter_vertical.restoreState(state_vertical) + else: + self.splitter_vertical.setSizes([120,200, 40]) + + state = settings.value("Mergin/VersionViewerSplitterSize") + if state: + QgsMessageLog.logMessage("horizonttal") + self.splitter.restoreState(state) + else: + height = max( + [self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()] + ) + self.splitter.setSizes([height, height]) + + + def set_mergin_client(self, mc): self.mc = mc From 7e8f31e8a70d45b69d384540b3ce241ddd020af4 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 4 Oct 2024 10:37:24 +0200 Subject: [PATCH 18/84] syntax error --- Mergin/version_viewer_dialog.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index bfd818f6..79723f5a 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -2,7 +2,7 @@ import os from qgis.PyQt import uic, QtCore -from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem, QPushButton, QMenu +from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem, QPushButton, QMenu, QMessageBox from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont from qgis.PyQt.QtCore import ( QStringListModel, @@ -396,7 +396,23 @@ def __init__(self,mc, parent=None): self.model.current_version = self.mp.version() self.fetch_from_server() + def exec(self): + try: + ws_id = self.mp.workspace_id() + except ClientError as e: + QMessageBox.warning(None, "Client Error", str(e)) + return + + # check if user has permissions + usage = self.mc.workspace_usage(ws_id) + if not usage["view_history"]["allowed"]: + QMessageBox.warning(None, "Permission Error", "The workspace does not allow to view project history.") + return + + self.reject() + return + def handle_click(self, index: QModelIndex): item = self.model.item_from_index(index) version_name = item["name"] From 8457b0ac882ff0d8e201962e133a6bfba6c0cb46 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 4 Oct 2024 11:09:37 +0200 Subject: [PATCH 19/84] Apply black formatting --- Mergin/diff.py | 3 + Mergin/diff_dialog.py | 2 - Mergin/plugin.py | 6 +- Mergin/project_history_dock.py | 72 ++++++++---------- Mergin/utils.py | 4 +- Mergin/version_viewer_dialog.py | 128 ++++++++++---------------------- 6 files changed, 77 insertions(+), 138 deletions(-) diff --git a/Mergin/diff.py b/Mergin/diff.py index 8d207a5f..a548faaf 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -108,6 +108,7 @@ def parse_db_schema(db_file): tables[tbl["table"]] = TableSchema(tbl["table"], columns) return tables + def db_schema_from_json(schema_json): """Create map of tables from the schema JSON file""" tables = {} # key: name, value: TableSchema @@ -119,6 +120,7 @@ def db_schema_from_json(schema_json): tables[tbl["table"]] = TableSchema(tbl["table"], columns) return tables + def parse_diff(geodiff, diff_file): """ Parse binary GeoDiff changeset and return map of changes per table @@ -372,6 +374,7 @@ def make_version_changes_layers(project_path, version): return layers + def find_changeset_file(file_name, version_dir): """Returns path to the diff file for the given version file""" for f in glob.iglob("*.gpkg-diff*", root_dir=version_dir): diff --git a/Mergin/diff_dialog.py b/Mergin/diff_dialog.py index ca571c65..6321548b 100644 --- a/Mergin/diff_dialog.py +++ b/Mergin/diff_dialog.py @@ -214,8 +214,6 @@ def zoom_selected(self): self.map_canvas.zoomToSelected([self.current_diff]) self.map_canvas.refresh() - - def show_unsaved_changes_warning(self): self.ui.messageBar.pushMessage( "Mergin", diff --git a/Mergin/plugin.py b/Mergin/plugin.py index 50eb3876..70f5cbb6 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -202,7 +202,6 @@ def initGui(self): self.iface.addDockWidget(Qt.RightDockWidgetArea, self.history_dock_widget) self.history_dock_widget.hide() - self.history_dock_widget = ProjectHistoryDockWidget(self.mc) self.iface.addDockWidget(Qt.RightDockWidgetArea, self.history_dock_widget) self.history_dock_widget.hide() @@ -323,7 +322,6 @@ def configure(self): self.show_browser_panel() self.history_dock_widget.set_mergin_client(self.mc) - def configure_db_sync(self): """Open db-sync setup wizard.""" project_path = QgsProject.instance().homePath() @@ -349,7 +347,7 @@ def configure_db_sync(self): wizard = DbSyncConfigWizard(project_name) if not wizard.exec_(): return - + def open_project_history_window(self): dlg = VersionViewerDialog(self.mc) dlg.exec_() @@ -575,7 +573,7 @@ def unload(self): self.iface.removeDockWidget(self.history_dock_widget) del self.history_dock_widget - + remove_project_variables() QgsExpressionContextUtils.removeGlobalVariable("mergin_username") QgsExpressionContextUtils.removeGlobalVariable("mergin_url") diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py index cadf8470..0ec104a4 100644 --- a/Mergin/project_history_dock.py +++ b/Mergin/project_history_dock.py @@ -19,23 +19,24 @@ from .version_viewer_dialog import VersionViewerDialog from .utils import ( - ClientError, - mergin_project_local_path, + ClientError, + mergin_project_local_path, check_mergin_subdirs, contextual_date, icon_path, - ) +) from .mergin.merginproject import MerginProject from .mergin.utils import int_version, is_versioned_file from .mergin import MerginClient + class VersionsTableModel(QAbstractTableModel): def __init__(self, parent=None): super().__init__(parent) - #Keep ordered + # Keep ordered self.versions = deque() self.oldest = None @@ -49,16 +50,15 @@ def latest_version(self): if len(self.versions) == 0: return None return int_version(self.versions[0]["name"]) - + def oldest_version(self): if len(self.versions) == 0: return None return int_version(self.versions[-1]["name"]) - def rowCount(self, parent: QModelIndex): return len(self.versions) - + def columnCount(self, parent: QModelIndex) -> int: return len(self.headers) @@ -66,7 +66,7 @@ def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.headers[section] return None - + def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None @@ -86,7 +86,7 @@ def data(self, index, role=Qt.DisplayRole): return font else: return None - + def insertRows(self, row, count, parent=QModelIndex()): self.beginInsertRows(parent, row, row + count - 1) self.endInsertRows() @@ -95,24 +95,24 @@ def clear(self): self.beginResetModel() self.versions.clear() self.endResetModel() - + def add_versions(self, versions): self.insertRows(len(self.versions) - 1, len(versions)) self.versions.extend(versions) self.layoutChanged.emit() - + def prepend_versions(self, versions): self.insertRows(0, len(versions)) self.versions.extendleft(versions) self.layoutChanged.emit() def canFetchMore(self, parent: QModelIndex) -> bool: - #Fetch while we are not the the first version - return self.oldest_version() == None or self.oldest_version() >= 1 + # Fetch while we are not the the first version + return self.oldest_version() == None or self.oldest_version() >= 1 def fetchMore(self, parent: QModelIndex) -> None: pass - #emit + # emit # fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) # fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) # fetcher.start() @@ -171,7 +171,7 @@ class VersionsFetcher(QThread): finished = pyqtSignal(list) - def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel, is_sync=False): + def __init__(self, mc: MerginClient, project_name, model: VersionsTableModel, is_sync=False): super(VersionsFetcher, self).__init__() self.mc = mc self.project_name = project_name @@ -179,21 +179,21 @@ def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel, self.is_sync = is_sync - self.per_page = 50 #server limit + self.per_page = 50 # server limit def run(self): - if (not self.is_sync): + if not self.is_sync: versions = self.fetch_previous() else: versions = self.fetch_sync_history() self.finished.emit(versions) - + def fetch_previous(self): if len(self.model.versions) == 0: - #initial fetch + # initial fetch info = self.mc.project_info(self.project_name) to = int_version(info["version"]) else: @@ -208,15 +208,15 @@ def fetch_previous(self): return versions def fetch_sync_history(self): - - #determine latest + + # determine latest info = self.mc.project_info(self.project_name) latest_server = int_version(info["version"]) since = self.model.latest_version() versions = self.mc.project_versions(self.project_name, since=since, to=latest_server) - versions.pop() #Remove the last as we already have it + versions.pop() # Remove the last as we already have it versions.reverse() return versions @@ -224,6 +224,7 @@ def fetch_sync_history(self): ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_history_dock.ui") + class ProjectHistoryDockWidget(QgsDockWidget): def __init__(self, mc): QgsDockWidget.__init__(self) @@ -233,16 +234,14 @@ def __init__(self, mc): self.mp = None self.project_path = mergin_project_local_path() - + self.fetcher = None self.diff_downloader = None - - self.model = VersionsTableModel() self.versions_tree.setModel(self.model) self.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) - + self.versions_tree.customContextMenuRequested.connect(self.show_context_menu) self.view_changes_btn.clicked.connect(self.show_version_viewer) @@ -253,8 +252,6 @@ def show_version_viewer(self): dlg = VersionViewerDialog() dlg.exec_() - - def update_ui(self): if self.mc is None: self.info_label.setText("Plugin is not configured.") @@ -265,7 +262,7 @@ def update_ui(self): self.info_label.setText("Current project is not saved. Project history is not available.") self.stackedWidget.setCurrentIndex(0) return - + if not check_mergin_subdirs(self.project_path): self.info_label.setText("Current project is not a Mergin project. Project history is not available.") self.stackedWidget.setCurrentIndex(0) @@ -290,10 +287,9 @@ def update_ui(self): self.stackedWidget.setCurrentIndex(1) - self.model.current_version = self.mp.version() self.fetch_from_server() - + def fetch_from_server(self): if self.fetcher and self.fetcher.isRunning(): @@ -303,7 +299,7 @@ def fetch_from_server(self): self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.model) self.fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) self.fetcher.start() - + def fetch_sync_server(self): if self.fetcher and self.fetcher.isRunning(): @@ -313,14 +309,11 @@ def fetch_sync_server(self): self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.model, is_sync=True) self.fetcher.finished.connect(lambda versions: self.model.prepend_versions(versions)) self.fetcher.start() - def on_scrollbar_changed(self, value): if self.ui.versions_tree.verticalScrollBar().maximum() <= value: self.fetch_from_server() - - def show_context_menu(self, pos): """Shows context menu in the project history dock""" index = self.versions_tree.indexAt(pos) @@ -339,13 +332,8 @@ def show_context_menu(self, pos): view_changes_action.setIcon(QIcon(icon_path("file-diff.svg"))) view_changes_action.triggered.connect(lambda: self.view_changes(version)) - - - menu.exec_(self.versions_tree.mapToGlobal(pos)) - - def version_details(self, version): """Shows version information with full view of added/updated/removed files""" @@ -383,8 +371,8 @@ def show_diff_viewer(self, version, msg=""): def set_mergin_client(self, mc): self.mc = mc - + def on_qgis_project_changed(self): self.model.clear() self.project_path = mergin_project_local_path() - self.update_ui() \ No newline at end of file + self.update_ui() diff --git a/Mergin/utils.py b/Mergin/utils.py index ffc8ef33..2123f169 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1515,6 +1515,7 @@ def check_mergin_subdirs(directory): return os.path.join(root, name) return False + def contextual_date(date_string, start_date=None): """Converts datetime string returned by the server into contextual duration string, e.g. 'N hours/days/month ago' @@ -1544,7 +1545,8 @@ def contextual_date(date_string, start_date=None): return f"{delta.days} {'days' if delta.days > 1 else 'day'} ago" + def format_datetime(date_string): """Formats datetime string returned by the server into human-readable format""" dt = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%SZ") - return dt.strftime("%a, %d %b %Y %H:%M:%S GMT") \ No newline at end of file + return dt.strftime("%a, %d %b %Y %H:%M:%S GMT") diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 79723f5a..99d52f1c 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -4,24 +4,10 @@ from qgis.PyQt import uic, QtCore from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem, QPushButton, QMenu, QMessageBox from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont -from qgis.PyQt.QtCore import ( - QStringListModel, - Qt, - QSettings, - QModelIndex, - QAbstractTableModel, - QThread, - pyqtSignal - ) +from qgis.PyQt.QtCore import QStringListModel, Qt, QSettings, QModelIndex, QAbstractTableModel, QThread, pyqtSignal from qgis.utils import iface -from qgis.core import ( - QgsProject, - QgsMessageLog, - QgsApplication, - QgsFeatureRequest, - QgsVectorLayerCache -) +from qgis.core import QgsProject, QgsMessageLog, QgsApplication, QgsFeatureRequest, QgsVectorLayerCache from qgis.gui import QgsGui, QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel @@ -48,7 +34,7 @@ contextual_date, is_versioned_file, icon_path, - format_datetime + format_datetime, ) from .mergin.merginproject import MerginProject @@ -64,11 +50,11 @@ class VersionsTableModel(QAbstractTableModel): VERSION = Qt.UserRole + 1 VERSION_NAME = Qt.UserRole + 2 - + def __init__(self, parent=None): super().__init__(parent) - #Keep ordered + # Keep ordered self.versions = deque() self.oldest = None @@ -82,16 +68,15 @@ def latest_version(self): if len(self.versions) == 0: return None return int_version(self.versions[0]["name"]) - + def oldest_version(self): if len(self.versions) == 0: return None return int_version(self.versions[-1]["name"]) - def rowCount(self, parent: QModelIndex): return len(self.versions) - + def columnCount(self, parent: QModelIndex) -> int: return len(self.headers) @@ -99,7 +84,7 @@ def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.headers[section] return None - + def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None @@ -126,7 +111,7 @@ def data(self, index, role=Qt.DisplayRole): return self.versions[idx]["name"] else: return None - + def insertRows(self, row, count, parent=QModelIndex()): self.beginInsertRows(parent, row, row + count - 1) self.endInsertRows() @@ -135,24 +120,24 @@ def clear(self): self.beginResetModel() self.versions.clear() self.endResetModel() - + def add_versions(self, versions): self.insertRows(len(self.versions) - 1, len(versions)) self.versions.extend(versions) self.layoutChanged.emit() - + def prepend_versions(self, versions): self.insertRows(0, len(versions)) self.versions.extendleft(versions) self.layoutChanged.emit() def canFetchMore(self, parent: QModelIndex) -> bool: - #Fetch while we are not the the first version - return self.oldest_version() == None or self.oldest_version() >= 1 + # Fetch while we are not the the first version + return self.oldest_version() == None or self.oldest_version() >= 1 def fetchMore(self, parent: QModelIndex) -> None: pass - #emit + # emit # fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) # fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) # fetcher.start() @@ -161,7 +146,6 @@ def item_from_index(self, index): return self.versions[index.row()] - class ChangesetsDownloader(QThread): """ Class to download version changesets in background worker thread @@ -212,7 +196,7 @@ class VersionsFetcher(QThread): finished = pyqtSignal(list) - def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel, is_sync=False): + def __init__(self, mc: MerginClient, project_name, model: VersionsTableModel, is_sync=False): super(VersionsFetcher, self).__init__() self.mc = mc self.project_name = project_name @@ -220,21 +204,21 @@ def __init__(self, mc : MerginClient , project_name, model: VersionsTableModel, self.is_sync = is_sync - self.per_page = 50 #server limit + self.per_page = 50 # server limit def run(self): - if (not self.is_sync): + if not self.is_sync: versions = self.fetch_previous() else: versions = self.fetch_sync_history() self.finished.emit(versions) - + def fetch_previous(self): if len(self.model.versions) == 0: - #initial fetch + # initial fetch info = self.mc.project_info(self.project_name) to = int_version(info["version"]) else: @@ -249,34 +233,32 @@ def fetch_previous(self): return versions def fetch_sync_history(self): - - #determine latest + + # determine latest info = self.mc.project_info(self.project_name) latest_server = int_version(info["version"]) since = self.model.latest_version() versions = self.mc.project_versions(self.project_name, since=since, to=latest_server) - versions.pop() #Remove the last as we already have it + versions.pop() # Remove the last as we already have it versions.reverse() return versions class VersionViewerDialog(QDialog): - def __init__(self,mc, parent=None): - + def __init__(self, mc, parent=None): + QDialog.__init__(self, parent) self.ui = uic.loadUi(ui_file, self) - self.mc = mc self.mp = None self.project_path = mergin_project_local_path() self.mp = MerginProject(self.project_path) - self.fetcher = None self.diff_downloader = None @@ -285,28 +267,24 @@ def __init__(self,mc, parent=None): # self.list_model = QStringListModel() # self.list_model.setStringList(["Line example\n 2 add 2 removed", "New_scratch_layer"]) - + # self.layer_list.setModel(self.list_model) - self.history_treeview.clicked.connect(self.handle_click) # self.history_treeview.currentChanged.connect(lambda index, _previous : self.handle_click(index)) self.fetch_from_server() - - height = 30 self.toolbar.setMinimumHeight(height) self.history_control.setMinimumHeight(height) self.history_control.setVisible(False) # self.verticalSpacer_3.setMinimumHeight(100) - self.verticalLayout.insertSpacing(0,height) + self.verticalLayout.insertSpacing(0, height) # self.map_canvas.setDisabled(False) # self.map_canvas.setEnabled(False) - self.toggle_layers_action = QAction( QgsApplication.getThemeIcon("/mActionAddLayer.svg"), "Toggle Project Layers", self ) @@ -317,9 +295,7 @@ def __init__(self,mc, parent=None): self.toolbar.addSeparator() - self.zoom_full_action = QAction( - QgsApplication.getThemeIcon("/mActionZoomFullExtent.svg"), "Zoom Full", self - ) + self.zoom_full_action = QAction(QgsApplication.getThemeIcon("/mActionZoomFullExtent.svg"), "Zoom Full", self) self.toolbar.addAction(self.zoom_full_action) self.zoom_selected_action = QAction( @@ -327,13 +303,10 @@ def __init__(self,mc, parent=None): ) self.toolbar.addAction(self.zoom_selected_action) - btn_add_changes = QPushButton("Add to project") btn_add_changes.setIcon(QgsApplication.getThemeIcon("/mActionAdd.svg")) menu = QMenu() - add_current_action = menu.addAction( - QIcon(icon_path("file-plus.svg")), "Add current changes layer to project" - ) + add_current_action = menu.addAction(QIcon(icon_path("file-plus.svg")), "Add current changes layer to project") add_current_action.triggered.connect(self.add_current_to_project) add_all_action = menu.addAction(QIcon(icon_path("folder-plus.svg")), "Add all changes layers to project") add_all_action.triggered.connect(self.add_all_to_project) @@ -342,44 +315,30 @@ def __init__(self,mc, parent=None): self.toolbar.addWidget(btn_add_changes) self.toolbar.setIconSize(iface.iconSize()) - - self.set_splitters_state() - - - - - - # self.history_verticalLayout.hide() # self.splitter_vertical.setCollapsible(0, True) # self.tabWidget.tabBar().setDocumentMode(True) # self.tabWidget.tabBar().setExpanding(False) - + # self.splitter_vertical.setStretchFactor(0, 0) - + # self.attribute_table_2.hide() self.current_diff = None self.diff_layers = [] self.filter_model = None - - self.layer_list.currentRowChanged.connect(self.diff_layer_changed) self.show_version_changes(25) self.update_canvas(self.diff_layers) self.diff_layer_changed(1) - - - self.version_details = self.mc.project_version_info(self.mp.project_id(), "v25") - self.icons = { "added": "plus.svg", "removed": "trash.svg", @@ -395,7 +354,7 @@ def __init__(self,mc, parent=None): self.model.current_version = self.mp.version() self.fetch_from_server() - + def exec(self): try: @@ -412,7 +371,7 @@ def exec(self): self.reject() return - + def handle_click(self, index: QModelIndex): item = self.model.item_from_index(index) version_name = item["name"] @@ -422,17 +381,17 @@ def handle_click(self, index: QModelIndex): self.populate_details() self.details_treeview.expandAll() - # Reset layer list + # Reset layer list QgsMessageLog.logMessage("Rest list") # self.list_model.setStringList([]) # self.list_model.removeRows( 0, self.list_model.rowCount() ) self.layer_list.clear() - + self.show_version_changes(version) self.update_canvas(self.diff_layers) return self.model.data(index, VersionsTableModel.VERSION) - + def closeEvent(self, event): self.save_splitters_state() QDialog.closeEvent(self, event) @@ -448,19 +407,15 @@ def set_splitters_state(self): if state_vertical: self.splitter_vertical.restoreState(state_vertical) else: - self.splitter_vertical.setSizes([120,200, 40]) + self.splitter_vertical.setSizes([120, 200, 40]) state = settings.value("Mergin/VersionViewerSplitterSize") if state: QgsMessageLog.logMessage("horizonttal") self.splitter.restoreState(state) else: - height = max( - [self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()] - ) + height = max([self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()]) self.splitter.setSizes([height, height]) - - def set_mergin_client(self, mc): self.mc = mc @@ -530,11 +485,9 @@ def update_canvas(self, layers): self.map_canvas.setExtent(extent) self.map_canvas.refresh() - def show_version_changes(self, version): self.diff_layers.clear() - layers = make_version_changes_layers(QgsProject.instance().homePath(), version) for vl in layers: self.diff_layers.append(vl) @@ -543,7 +496,6 @@ def show_version_changes(self, version): self.layer_list.addItem(QListWidgetItem(icon, f"{vl.name()} ({vl.featureCount()})\n 2 updated")) # self.tab_bar.addTab(self.icon_for_layer(vl), f"{vl.name()} ({vl.featureCount()})") - if len(self.diff_layers) >= 1: QgsMessageLog.logMessage("Visual change") @@ -559,7 +511,6 @@ def show_version_changes(self, version): # self.list_model.setStringList([vl.name() for vl in self.diff_layers]) # self.tab_bar.setCurrentIndex(0) - def collect_layers(self, checked): if checked: layers = iface.mapCanvas().layers() @@ -611,8 +562,8 @@ def add_all_to_project(self): def keyPressEvent(self, event): QgsMessageLog.logMessage("eventxezdede") - - if event.key() == QtCore.Qt.Key_C: + + if event.key() == QtCore.Qt.Key_C: QgsMessageLog.logMessage("blabla" + str(self.stackedWidget.currentIndex())) if self.stackedWidget.currentIndex() == 0: self.stackedWidget.setCurrentIndex(1) @@ -622,4 +573,3 @@ def keyPressEvent(self, event): self.tabWidget.setCurrentIndex(0) return - From 5d161cfd7469feaa821b512faeff20f27d369d4a Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 4 Oct 2024 14:05:33 +0200 Subject: [PATCH 20/84] FIx view not updated when changed with keyboard and cleanup --- Mergin/version_viewer_dialog.py | 60 ++++++--------------------------- 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 99d52f1c..1a54ca4c 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -4,7 +4,7 @@ from qgis.PyQt import uic, QtCore from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem, QPushButton, QMenu, QMessageBox from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont -from qgis.PyQt.QtCore import QStringListModel, Qt, QSettings, QModelIndex, QAbstractTableModel, QThread, pyqtSignal +from qgis.PyQt.QtCore import QStringListModel, Qt, QSettings, QModelIndex, QAbstractTableModel, QThread, pyqtSignal, QItemSelectionModel from qgis.utils import iface from qgis.core import QgsProject, QgsMessageLog, QgsApplication, QgsFeatureRequest, QgsVectorLayerCache @@ -264,14 +264,11 @@ def __init__(self, mc, parent=None): self.model = VersionsTableModel() self.history_treeview.setModel(self.model) + + self.selectionModel = self.history_treeview.selectionModel() - # self.list_model = QStringListModel() - # self.list_model.setStringList(["Line example\n 2 add 2 removed", "New_scratch_layer"]) + self.selectionModel.currentChanged.connect(self.currentVersionChanged) - # self.layer_list.setModel(self.list_model) - - self.history_treeview.clicked.connect(self.handle_click) - # self.history_treeview.currentChanged.connect(lambda index, _previous : self.handle_click(index)) self.fetch_from_server() height = 30 @@ -279,11 +276,6 @@ def __init__(self, mc, parent=None): self.history_control.setMinimumHeight(height) self.history_control.setVisible(False) - # self.verticalSpacer_3.setMinimumHeight(100) - self.verticalLayout.insertSpacing(0, height) - - # self.map_canvas.setDisabled(False) - # self.map_canvas.setEnabled(False) self.toggle_layers_action = QAction( QgsApplication.getThemeIcon("/mActionAddLayer.svg"), "Toggle Project Layers", self @@ -317,16 +309,6 @@ def __init__(self, mc, parent=None): self.set_splitters_state() - # self.history_verticalLayout.hide() - # self.splitter_vertical.setCollapsible(0, True) - - # self.tabWidget.tabBar().setDocumentMode(True) - # self.tabWidget.tabBar().setExpanding(False) - - # self.splitter_vertical.setStretchFactor(0, 0) - - # self.attribute_table_2.hide() - self.current_diff = None self.diff_layers = [] self.filter_model = None @@ -372,8 +354,9 @@ def exec(self): self.reject() return - def handle_click(self, index: QModelIndex): - item = self.model.item_from_index(index) + + def currentVersionChanged(self, current_index, previous_index): + item = self.model.item_from_index(current_index) version_name = item["name"] version = int_version(item["name"]) @@ -382,15 +365,12 @@ def handle_click(self, index: QModelIndex): self.details_treeview.expandAll() # Reset layer list - QgsMessageLog.logMessage("Rest list") - # self.list_model.setStringList([]) - # self.list_model.removeRows( 0, self.list_model.rowCount() ) self.layer_list.clear() self.show_version_changes(version) self.update_canvas(self.diff_layers) - return self.model.data(index, VersionsTableModel.VERSION) + return self.model.data(current_index, VersionsTableModel.VERSION) def closeEvent(self, event): self.save_splitters_state() @@ -411,7 +391,6 @@ def set_splitters_state(self): state = settings.value("Mergin/VersionViewerSplitterSize") if state: - QgsMessageLog.logMessage("horizonttal") self.splitter.restoreState(state) else: height = max([self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()]) @@ -491,25 +470,21 @@ def show_version_changes(self, version): layers = make_version_changes_layers(QgsProject.instance().homePath(), version) for vl in layers: self.diff_layers.append(vl) - QgsMessageLog.logMessage(f"{vl.name()}") icon = icon_for_layer(vl) self.layer_list.addItem(QListWidgetItem(icon, f"{vl.name()} ({vl.featureCount()})\n 2 updated")) - # self.tab_bar.addTab(self.icon_for_layer(vl), f"{vl.name()} ({vl.featureCount()})") if len(self.diff_layers) >= 1: - QgsMessageLog.logMessage("Visual change") self.toolbar.setEnabled(True) self.layer_list.setCurrentRow(0) self.stackedWidget.setCurrentIndex(0) self.tabWidget.setCurrentIndex(0) + self.tabWidget.setTabEnabled(0, True) else: - QgsMessageLog.logMessage("no Visual change") self.toolbar.setEnabled(False) self.stackedWidget.setCurrentIndex(1) self.tabWidget.setCurrentIndex(1) - # self.list_model.setStringList([vl.name() for vl in self.diff_layers]) - # self.tab_bar.setCurrentIndex(0) + self.tabWidget.setTabEnabled(0, False) def collect_layers(self, checked): if checked: @@ -549,7 +524,6 @@ def diff_layer_changed(self, index): self.attribute_table.setAttributeTableConfig(config) layers = self.collect_layers(self.toggle_layers_action.isChecked()) - # layers = self.collect_layers(True) self.update_canvas(layers) def add_current_to_project(self): @@ -559,17 +533,3 @@ def add_current_to_project(self): def add_all_to_project(self): for layer in self.diff_layers: QgsProject.instance().addMapLayer(layer) - - def keyPressEvent(self, event): - QgsMessageLog.logMessage("eventxezdede") - - if event.key() == QtCore.Qt.Key_C: - QgsMessageLog.logMessage("blabla" + str(self.stackedWidget.currentIndex())) - if self.stackedWidget.currentIndex() == 0: - self.stackedWidget.setCurrentIndex(1) - self.tabWidget.setCurrentIndex(1) - else: - self.stackedWidget.setCurrentIndex(0) - self.tabWidget.setCurrentIndex(0) - - return From 88652c0f2bd0a0e324fb382815e3f58333c49877 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 8 Oct 2024 13:42:50 +0200 Subject: [PATCH 21/84] Fix default version openned and reorder things --- Mergin/version_viewer_dialog.py | 95 +++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 1a54ca4c..a0a27307 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -248,6 +248,13 @@ def fetch_sync_history(self): class VersionViewerDialog(QDialog): + """ + The class is constructed in a way that the flow of the code follow the flow the UI + The UI is read from left to right and each splitter is read from top to bottom + + The __init__ method follow this pattern after varaible initiatlization + the methods of the class also follow this pattern + """ def __init__(self, mc, parent=None): QDialog.__init__(self, parent) @@ -262,15 +269,20 @@ def __init__(self, mc, parent=None): self.fetcher = None self.diff_downloader = None - self.model = VersionsTableModel() - self.history_treeview.setModel(self.model) - - self.selectionModel = self.history_treeview.selectionModel() + self.set_splitters_state() - self.selectionModel.currentChanged.connect(self.currentVersionChanged) + self.versionModel = VersionsTableModel() + self.history_treeview.setModel(self.versionModel) + self.history_treeview.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) + + self.selectionModel:QItemSelectionModel = self.history_treeview.selectionModel() + self.selectionModel.currentChanged.connect(self.current_version_changed) + self.has_selected_latest = False + self.fetch_from_server() + height = 30 self.toolbar.setMinimumHeight(height) @@ -307,20 +319,13 @@ def __init__(self, mc, parent=None): self.toolbar.addWidget(btn_add_changes) self.toolbar.setIconSize(iface.iconSize()) - self.set_splitters_state() + self.current_diff = None self.diff_layers = [] self.filter_model = None - self.layer_list.currentRowChanged.connect(self.diff_layer_changed) - self.show_version_changes(25) - self.update_canvas(self.diff_layers) - self.diff_layer_changed(1) - - self.version_details = self.mc.project_version_info(self.mp.project_id(), "v25") - self.icons = { "added": "plus.svg", "removed": "trash.svg", @@ -331,14 +336,10 @@ def __init__(self, mc, parent=None): self.model_detail = QStandardItemModel() self.model_detail.setHorizontalHeaderLabels(["Details"]) self.details_treeview.setModel(self.model_detail) - self.populate_details() - self.details_treeview.expandAll() - self.model.current_version = self.mp.version() - self.fetch_from_server() + self.versionModel.current_version = self.mp.version() def exec(self): - try: ws_id = self.mp.workspace_id() except ClientError as e: @@ -354,23 +355,6 @@ def exec(self): self.reject() return - - def currentVersionChanged(self, current_index, previous_index): - item = self.model.item_from_index(current_index) - version_name = item["name"] - version = int_version(item["name"]) - - self.version_details = self.mc.project_version_info(self.mp.project_id(), version_name) - self.populate_details() - self.details_treeview.expandAll() - - # Reset layer list - self.layer_list.clear() - - self.show_version_changes(version) - self.update_canvas(self.diff_layers) - - return self.model.data(current_index, VersionsTableModel.VERSION) def closeEvent(self, event): self.save_splitters_state() @@ -396,19 +380,50 @@ def set_splitters_state(self): height = max([self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()]) self.splitter.setSizes([height, height]) - def set_mergin_client(self, mc): - self.mc = mc - def fetch_from_server(self): if self.fetcher and self.fetcher.isRunning(): # Only fetching when previous is finshed return - self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.model) - self.fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) + self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.versionModel) + self.fetcher.finished.connect(lambda versions: self.versionModel.add_versions(versions)) + self.fetcher.finished.connect(lambda versions: self.selected_latest()) self.fetcher.start() + def on_scrollbar_changed(self, value): + if self.ui.history_treeview.verticalScrollBar().maximum() <= value: + self.fetch_from_server() + + def selected_latest(self): + # On open dialog select the latest version + if self.has_selected_latest and self.has_selected_latest == True: + return + + self.has_selected_latest = True + + index = self.versionModel.index(0,0) + self.selectionModel.setCurrentIndex(index, QItemSelectionModel.Select | QItemSelectionModel.Rows) + + + def current_version_changed(self, current_index, previous_index): + #Update the ui when the selected version change + item = self.versionModel.item_from_index(current_index) + version_name = item["name"] + version = int_version(item["name"]) + + self.version_details = self.mc.project_version_info(self.mp.project_id(), version_name) + self.populate_details() + self.details_treeview.expandAll() + + # Reset layer list + self.layer_list.clear() + + self.show_version_changes(version) + self.update_canvas(self.diff_layers) + + return self.versionModel.data(current_index, VersionsTableModel.VERSION) + def populate_details(self): self.edit_project_size.setText(bytes_to_human_size(self.version_details["project_size"])) self.edit_created.setText(format_datetime(self.version_details["created"])) From c9a55ce2ae38bae888ac533149adcf8dcca045d2 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 8 Oct 2024 13:47:54 +0200 Subject: [PATCH 22/84] add control to map --- Mergin/version_viewer_dialog.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index a0a27307..eca9ee54 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -3,7 +3,7 @@ from qgis.PyQt import uic, QtCore from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem, QPushButton, QMenu, QMessageBox -from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont +from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont, QColor from qgis.PyQt.QtCore import QStringListModel, Qt, QSettings, QModelIndex, QAbstractTableModel, QThread, pyqtSignal, QItemSelectionModel from qgis.utils import iface @@ -319,6 +319,10 @@ def __init__(self, mc, parent=None): self.toolbar.addWidget(btn_add_changes) self.toolbar.setIconSize(iface.iconSize()) + self.map_canvas.enableAntiAliasing(True) + self.map_canvas.setSelectionColor(QColor(Qt.cyan)) + self.pan_tool = QgsMapToolPan(self.map_canvas) + self.map_canvas.setMapTool(self.pan_tool) self.current_diff = None From e597a203b9a156404d527264a1d1ffb55b202394 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 8 Oct 2024 13:47:58 +0200 Subject: [PATCH 23/84] format --- Mergin/version_viewer_dialog.py | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index eca9ee54..c65356ea 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -4,7 +4,16 @@ from qgis.PyQt import uic, QtCore from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem, QPushButton, QMenu, QMessageBox from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont, QColor -from qgis.PyQt.QtCore import QStringListModel, Qt, QSettings, QModelIndex, QAbstractTableModel, QThread, pyqtSignal, QItemSelectionModel +from qgis.PyQt.QtCore import ( + QStringListModel, + Qt, + QSettings, + QModelIndex, + QAbstractTableModel, + QThread, + pyqtSignal, + QItemSelectionModel, +) from qgis.utils import iface from qgis.core import QgsProject, QgsMessageLog, QgsApplication, QgsFeatureRequest, QgsVectorLayerCache @@ -255,6 +264,7 @@ class VersionViewerDialog(QDialog): The __init__ method follow this pattern after varaible initiatlization the methods of the class also follow this pattern """ + def __init__(self, mc, parent=None): QDialog.__init__(self, parent) @@ -274,14 +284,13 @@ def __init__(self, mc, parent=None): self.versionModel = VersionsTableModel() self.history_treeview.setModel(self.versionModel) self.history_treeview.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) - - self.selectionModel:QItemSelectionModel = self.history_treeview.selectionModel() + + self.selectionModel: QItemSelectionModel = self.history_treeview.selectionModel() self.selectionModel.currentChanged.connect(self.current_version_changed) self.has_selected_latest = False - - self.fetch_from_server() + self.fetch_from_server() height = 30 self.toolbar.setMinimumHeight(height) @@ -324,7 +333,6 @@ def __init__(self, mc, parent=None): self.pan_tool = QgsMapToolPan(self.map_canvas) self.map_canvas.setMapTool(self.pan_tool) - self.current_diff = None self.diff_layers = [] self.filter_model = None @@ -359,7 +367,6 @@ def exec(self): self.reject() return - def closeEvent(self, event): self.save_splitters_state() QDialog.closeEvent(self, event) @@ -392,26 +399,25 @@ def fetch_from_server(self): self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.versionModel) self.fetcher.finished.connect(lambda versions: self.versionModel.add_versions(versions)) - self.fetcher.finished.connect(lambda versions: self.selected_latest()) + self.fetcher.finished.connect(lambda versions: self.select_latest()) self.fetcher.start() def on_scrollbar_changed(self, value): if self.ui.history_treeview.verticalScrollBar().maximum() <= value: self.fetch_from_server() - def selected_latest(self): - # On open dialog select the latest version + def select_latest(self): + # On open dialog select the latest version if self.has_selected_latest and self.has_selected_latest == True: return - + self.has_selected_latest = True - - index = self.versionModel.index(0,0) - self.selectionModel.setCurrentIndex(index, QItemSelectionModel.Select | QItemSelectionModel.Rows) + index = self.versionModel.index(0, 0) + self.selectionModel.setCurrentIndex(index, QItemSelectionModel.Select | QItemSelectionModel.Rows) def current_version_changed(self, current_index, previous_index): - #Update the ui when the selected version change + # Update the ui when the selected version change item = self.versionModel.item_from_index(current_index) version_name = item["name"] version = int_version(item["name"]) From 2d64dd2baa9772d0d25800781768e20c74f18780 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 8 Oct 2024 13:53:44 +0200 Subject: [PATCH 24/84] add todo --- Mergin/version_viewer_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index c65356ea..4320e042 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -497,7 +497,8 @@ def show_version_changes(self, version): self.diff_layers.append(vl) icon = icon_for_layer(vl) - self.layer_list.addItem(QListWidgetItem(icon, f"{vl.name()} ({vl.featureCount()})\n 2 updated")) + self.layer_list.addItem(QListWidgetItem(icon, f"{vl.name()} ({vl.featureCount()})")) + # evenutally add a summary eg: \n 2 updated" if len(self.diff_layers) >= 1: self.toolbar.setEnabled(True) From e2adcb6d02e3b8fd43a7a847dfb291b57d023545 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 8 Oct 2024 14:19:24 +0200 Subject: [PATCH 25/84] Hide from the plugin the old history dock --- Mergin/plugin.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Mergin/plugin.py b/Mergin/plugin.py index 70f5cbb6..19a75ea1 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -89,7 +89,7 @@ def __init__(self, iface): self.current_workspace = dict() self.provider = MerginProvider() - self.history_dock_widget = None + # self.history_dock_widget = None if self.iface is not None: self.toolbar = self.iface.addToolBar("Mergin Maps Toolbar") @@ -198,13 +198,9 @@ def initGui(self): self.iface.addCustomActionForLayerType(self.action_export_mbtiles, "", QgsMapLayer.VectorTileLayer, False) self.action_export_mbtiles.triggered.connect(self.export_vector_tiles) - self.history_dock_widget = ProjectHistoryDockWidget(self.mc) - self.iface.addDockWidget(Qt.RightDockWidgetArea, self.history_dock_widget) - self.history_dock_widget.hide() - - self.history_dock_widget = ProjectHistoryDockWidget(self.mc) - self.iface.addDockWidget(Qt.RightDockWidgetArea, self.history_dock_widget) - self.history_dock_widget.hide() + # self.history_dock_widget = ProjectHistoryDockWidget(self.mc) + # self.iface.addDockWidget(Qt.RightDockWidgetArea, self.history_dock_widget) + # self.history_dock_widget.hide() QgsProject.instance().layersAdded.connect(self.add_context_menu_actions) @@ -320,7 +316,7 @@ def configure(self): self.mc = dlg.writeSettings() self.on_config_changed() self.show_browser_panel() - self.history_dock_widget.set_mergin_client(self.mc) + # self.history_dock_widget.set_mergin_client(self.mc) def configure_db_sync(self): """Open db-sync setup wizard.""" @@ -352,8 +348,8 @@ def open_project_history_window(self): dlg = VersionViewerDialog(self.mc) dlg.exec_() - def toggle_project_history_dock(self): - self.history_dock_widget.toggleUserVisible() + # def toggle_project_history_dock(self): + # self.history_dock_widget.toggleUserVisible() def show_no_workspaces_dialog(self): msg = ( @@ -490,7 +486,7 @@ def create_new_project(self): def current_project_sync(self): """Synchronise current Mergin Maps project.""" self.manager.project_status(self.mergin_proj_dir) - self.history_dock_widget.fetch_sync_server() + # self.history_dock_widget.fetch_sync_server() def find_project(self): """Open new Find Mergin Maps project dialog""" @@ -545,8 +541,8 @@ def on_qgis_project_changed(self): self.mergin_proj_dir = mergin_project_local_path() if self.mergin_proj_dir is not None: self.enable_toolbar_actions() - if self.history_dock_widget: - self.history_dock_widget.on_qgis_project_changed() + # if self.history_dock_widget: + # self.history_dock_widget.on_qgis_project_changed() def add_context_menu_actions(self, layers): provider_names = "vectortile" @@ -571,8 +567,8 @@ def unload(self): self.iface.unregisterProjectPropertiesWidgetFactory(self.mergin_project_config_factory) - self.iface.removeDockWidget(self.history_dock_widget) - del self.history_dock_widget + # self.iface.removeDockWidget(self.history_dock_widget) + # del self.history_dock_widget remove_project_variables() QgsExpressionContextUtils.removeGlobalVariable("mergin_username") From 730647905a2144dd49b500213cc08b4bf4973536 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 9 Oct 2024 13:31:45 +0200 Subject: [PATCH 26/84] fix version were not cached --- Mergin/version_viewer_dialog.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 4320e042..2b231067 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -429,7 +429,16 @@ def current_version_changed(self, current_index, previous_index): # Reset layer list self.layer_list.clear() - self.show_version_changes(version) + if not os.path.exists(os.path.join(self.project_path, ".mergin", ".cache", f"v{version}")): + if self.diff_downloader and self.diff_downloader.isRunning(): + self.diff_downloader.requestInterruption() + + self.diff_downloader = ChangesetsDownloader(self.mc, self.mp, version) + self.diff_downloader.finished.connect(lambda msg: self.show_version_changes(version)) + self.diff_downloader.start() + else: + self.show_version_changes(version) + self.update_canvas(self.diff_layers) return self.versionModel.data(current_index, VersionsTableModel.VERSION) From 16e2ee6c18df47b153f09ca2a9002cac46e23df9 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 9 Oct 2024 13:37:38 +0200 Subject: [PATCH 27/84] Remove previous dock --- Mergin/plugin.py | 17 - Mergin/project_history_dock.py | 378 ------------------ Mergin/ui/ui_version_details_dialog.ui | 147 ------- ...rsions_viewer.ui => ui_versions_viewer.ui} | 0 Mergin/version_details_dialog.py | 77 ---- Mergin/version_viewer_dialog.py | 2 +- 6 files changed, 1 insertion(+), 620 deletions(-) delete mode 100644 Mergin/project_history_dock.py delete mode 100644 Mergin/ui/ui_version_details_dialog.ui rename Mergin/ui/{ui_draft_versions_viewer.ui => ui_versions_viewer.ui} (100%) delete mode 100644 Mergin/version_details_dialog.py diff --git a/Mergin/plugin.py b/Mergin/plugin.py index 19a75ea1..e88458b7 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -42,7 +42,6 @@ from .sync_dialog import SyncDialog from .configure_sync_wizard import DbSyncConfigWizard from .remove_project_dialog import RemoveProjectDialog -from .project_history_dock import ProjectHistoryDockWidget from .version_viewer_dialog import VersionViewerDialog from .utils import ( ServerType, @@ -89,8 +88,6 @@ def __init__(self, iface): self.current_workspace = dict() self.provider = MerginProvider() - # self.history_dock_widget = None - if self.iface is not None: self.toolbar = self.iface.addToolBar("Mergin Maps Toolbar") self.toolbar.setToolTip("Mergin Maps Toolbar") @@ -198,10 +195,6 @@ def initGui(self): self.iface.addCustomActionForLayerType(self.action_export_mbtiles, "", QgsMapLayer.VectorTileLayer, False) self.action_export_mbtiles.triggered.connect(self.export_vector_tiles) - # self.history_dock_widget = ProjectHistoryDockWidget(self.mc) - # self.iface.addDockWidget(Qt.RightDockWidgetArea, self.history_dock_widget) - # self.history_dock_widget.hide() - QgsProject.instance().layersAdded.connect(self.add_context_menu_actions) def add_action( @@ -316,7 +309,6 @@ def configure(self): self.mc = dlg.writeSettings() self.on_config_changed() self.show_browser_panel() - # self.history_dock_widget.set_mergin_client(self.mc) def configure_db_sync(self): """Open db-sync setup wizard.""" @@ -348,9 +340,6 @@ def open_project_history_window(self): dlg = VersionViewerDialog(self.mc) dlg.exec_() - # def toggle_project_history_dock(self): - # self.history_dock_widget.toggleUserVisible() - def show_no_workspaces_dialog(self): msg = ( "Workspace is a place to store your projects and share them with your colleagues. " @@ -486,7 +475,6 @@ def create_new_project(self): def current_project_sync(self): """Synchronise current Mergin Maps project.""" self.manager.project_status(self.mergin_proj_dir) - # self.history_dock_widget.fetch_sync_server() def find_project(self): """Open new Find Mergin Maps project dialog""" @@ -541,8 +529,6 @@ def on_qgis_project_changed(self): self.mergin_proj_dir = mergin_project_local_path() if self.mergin_proj_dir is not None: self.enable_toolbar_actions() - # if self.history_dock_widget: - # self.history_dock_widget.on_qgis_project_changed() def add_context_menu_actions(self, layers): provider_names = "vectortile" @@ -567,9 +553,6 @@ def unload(self): self.iface.unregisterProjectPropertiesWidgetFactory(self.mergin_project_config_factory) - # self.iface.removeDockWidget(self.history_dock_widget) - # del self.history_dock_widget - remove_project_variables() QgsExpressionContextUtils.removeGlobalVariable("mergin_username") QgsExpressionContextUtils.removeGlobalVariable("mergin_url") diff --git a/Mergin/project_history_dock.py b/Mergin/project_history_dock.py deleted file mode 100644 index 0ec104a4..00000000 --- a/Mergin/project_history_dock.py +++ /dev/null @@ -1,378 +0,0 @@ -import os -import math -import json -from urllib.error import URLError -from collections import deque - -from PyQt5.QtCore import QObject -from qgis.PyQt import uic -from qgis.PyQt.QtGui import QIcon, QFont -from qgis.PyQt.QtCore import Qt, QThread, pyqtSignal, QModelIndex, QAbstractTableModel -from qgis.PyQt.QtWidgets import QMenu, QMessageBox - -from qgis.gui import QgsDockWidget -from qgis.core import Qgis, QgsMessageLog -from qgis.utils import iface - -from .diff_dialog import DiffViewerDialog -from .version_details_dialog import VersionDetailsDialog -from .version_viewer_dialog import VersionViewerDialog - -from .utils import ( - ClientError, - mergin_project_local_path, - check_mergin_subdirs, - contextual_date, - icon_path, -) - -from .mergin.merginproject import MerginProject -from .mergin.utils import int_version, is_versioned_file - -from .mergin import MerginClient - - -class VersionsTableModel(QAbstractTableModel): - def __init__(self, parent=None): - super().__init__(parent) - - # Keep ordered - self.versions = deque() - - self.oldest = None - self.latest = None - - self.headers = ["Version", "Author", "Created"] - - self.current_version = None - - def latest_version(self): - if len(self.versions) == 0: - return None - return int_version(self.versions[0]["name"]) - - def oldest_version(self): - if len(self.versions) == 0: - return None - return int_version(self.versions[-1]["name"]) - - def rowCount(self, parent: QModelIndex): - return len(self.versions) - - def columnCount(self, parent: QModelIndex) -> int: - return len(self.headers) - - def headerData(self, section, orientation, role): - if orientation == Qt.Horizontal and role == Qt.DisplayRole: - return self.headers[section] - return None - - def data(self, index, role=Qt.DisplayRole): - if not index.isValid(): - return None - - idx = index.row() - if role == Qt.DisplayRole: - if index.column() == 0: - return self.versions[idx]["name"] - if index.column() == 1: - return self.versions[idx]["author"] - if index.column() == 2: - return contextual_date(self.versions[idx]["created"]) - elif role == Qt.FontRole: - if self.versions[idx]["name"] == self.current_version: - font = QFont() - font.setBold(True) - return font - else: - return None - - def insertRows(self, row, count, parent=QModelIndex()): - self.beginInsertRows(parent, row, row + count - 1) - self.endInsertRows() - - def clear(self): - self.beginResetModel() - self.versions.clear() - self.endResetModel() - - def add_versions(self, versions): - self.insertRows(len(self.versions) - 1, len(versions)) - self.versions.extend(versions) - self.layoutChanged.emit() - - def prepend_versions(self, versions): - self.insertRows(0, len(versions)) - self.versions.extendleft(versions) - self.layoutChanged.emit() - - def canFetchMore(self, parent: QModelIndex) -> bool: - # Fetch while we are not the the first version - return self.oldest_version() == None or self.oldest_version() >= 1 - - def fetchMore(self, parent: QModelIndex) -> None: - pass - # emit - # fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) - # fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) - # fetcher.start() - - def item_from_index(self, index): - return self.versions[index.row()] - - -class ChangesetsDownloader(QThread): - """ - Class to download version changesets in background worker thread - """ - - finished = pyqtSignal(str) - - def __init__(self, mc, mp, version): - """ - ChangesetsDownloader constructor - - :param mc: MerginClient instance - :param mp: MerginProject instance - :param version: project version to download - """ - super(ChangesetsDownloader, self).__init__() - self.mc = mc - self.mp = mp - self.version = version - - def run(self): - info = self.mc.project_info(self.mp.project_full_name(), version=f"v{self.version}") - files = [f for f in info["files"] if is_versioned_file(f["path"])] - if not files: - self.finished.emit("This version does not contain changes in the project layers.") - return - - has_history = any("diff" in f for f in files) - if not has_history: - self.finished.emit("This version does not contain changes in the project layers.") - return - - for f in files: - if self.isInterruptionRequested(): - return - - if "diff" not in f: - continue - file_diffs = self.mc.download_file_diffs(self.mp.dir, f["path"], [f"v{self.version}"]) - full_gpkg = self.mp.fpath_cache(f["path"], version=f"v{self.version}") - if not os.path.exists(full_gpkg): - self.mc.download_file(self.mp.dir, f["path"], full_gpkg, f"v{self.version}") - - self.finished.emit("") - - -class VersionsFetcher(QThread): - - finished = pyqtSignal(list) - - def __init__(self, mc: MerginClient, project_name, model: VersionsTableModel, is_sync=False): - super(VersionsFetcher, self).__init__() - self.mc = mc - self.project_name = project_name - self.model = model - - self.is_sync = is_sync - - self.per_page = 50 # server limit - - def run(self): - - if not self.is_sync: - versions = self.fetch_previous() - else: - versions = self.fetch_sync_history() - - self.finished.emit(versions) - - def fetch_previous(self): - - if len(self.model.versions) == 0: - # initial fetch - info = self.mc.project_info(self.project_name) - to = int_version(info["version"]) - else: - to = self.model.oldest_version() - since = to - 100 - if since < 0: - since = 1 - - versions = self.mc.project_versions(self.project_name, since=since, to=to) - versions.reverse() - - return versions - - def fetch_sync_history(self): - - # determine latest - info = self.mc.project_info(self.project_name) - - latest_server = int_version(info["version"]) - since = self.model.latest_version() - - versions = self.mc.project_versions(self.project_name, since=since, to=latest_server) - versions.pop() # Remove the last as we already have it - versions.reverse() - - return versions - - -ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_history_dock.ui") - - -class ProjectHistoryDockWidget(QgsDockWidget): - def __init__(self, mc): - QgsDockWidget.__init__(self) - self.ui = uic.loadUi(ui_file, self) - - self.mc = mc - self.mp = None - - self.project_path = mergin_project_local_path() - - self.fetcher = None - self.diff_downloader = None - - self.model = VersionsTableModel() - self.versions_tree.setModel(self.model) - self.versions_tree.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) - - self.versions_tree.customContextMenuRequested.connect(self.show_context_menu) - - self.view_changes_btn.clicked.connect(self.show_version_viewer) - - self.update_ui() - - def show_version_viewer(self): - dlg = VersionViewerDialog() - dlg.exec_() - - def update_ui(self): - if self.mc is None: - self.info_label.setText("Plugin is not configured.") - self.stackedWidget.setCurrentIndex(0) - return - - if self.project_path is None: - self.info_label.setText("Current project is not saved. Project history is not available.") - self.stackedWidget.setCurrentIndex(0) - return - - if not check_mergin_subdirs(self.project_path): - self.info_label.setText("Current project is not a Mergin project. Project history is not available.") - self.stackedWidget.setCurrentIndex(0) - return - - self.mp = MerginProject(self.project_path) - self.local_project_version = self.mp.version() - - try: - ws_id = self.mp.workspace_id() - except ClientError as e: - self.info_label.setText(str(e)) - self.stackedWidget.setCurrentIndex(0) - return - - # check if user has permissions - usage = self.mc.workspace_usage(ws_id) - if not usage["view_history"]["allowed"]: - self.info_label.setText("The workspace does not allow to view project history.") - self.stackedWidget.setCurrentIndex(0) - return - - self.stackedWidget.setCurrentIndex(1) - - self.model.current_version = self.mp.version() - self.fetch_from_server() - - def fetch_from_server(self): - - if self.fetcher and self.fetcher.isRunning(): - # Only fetching when previous is finshed - return - - self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.model) - self.fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) - self.fetcher.start() - - def fetch_sync_server(self): - - if self.fetcher and self.fetcher.isRunning(): - # Only fetching when previous is finshed - self.fetcher.requestInterruption() - - self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.model, is_sync=True) - self.fetcher.finished.connect(lambda versions: self.model.prepend_versions(versions)) - self.fetcher.start() - - def on_scrollbar_changed(self, value): - if self.ui.versions_tree.verticalScrollBar().maximum() <= value: - self.fetch_from_server() - - def show_context_menu(self, pos): - """Shows context menu in the project history dock""" - index = self.versions_tree.indexAt(pos) - if not index.isValid(): - return - - item = self.model.item_from_index(index) - version_name = item["name"] - version = int_version(item["name"]) - - menu = QMenu() - view_details_action = menu.addAction("Version details") - view_details_action.setIcon(QIcon(icon_path("file-description.svg"))) - view_details_action.triggered.connect(lambda: self.version_details(version_name)) - view_changes_action = menu.addAction("View changes") - view_changes_action.setIcon(QIcon(icon_path("file-diff.svg"))) - view_changes_action.triggered.connect(lambda: self.view_changes(version)) - - menu.exec_(self.versions_tree.mapToGlobal(pos)) - - def version_details(self, version): - """Shows version information with full view of added/updated/removed files""" - - data = self.mc.project_version_info(self.mp.project_id(), version) - dlg = VersionDetailsDialog(data) - dlg.exec_() - - def view_changes(self, version): - """Initiates download of changesets for the given version if they are not present - in the cache. Otherwise use cached changesets to show diff viewer dialog. - """ - if not os.path.exists(os.path.join(self.project_path, ".mergin", ".cache", f"v{version}")): - if self.diff_downloader and self.diff_downloader.isRunning(): - self.diff_downloader.requestInterruption() - - self.diff_downloader = ChangesetsDownloader(self.mc, self.mp, version) - self.diff_downloader.finished.connect(lambda msg: self.show_diff_viewer(version, msg)) - self.diff_downloader.start() - else: - self.show_diff_viewer(version) - - def show_diff_viewer(self, version, msg=""): - """Shows a Diff Viewer dialog with changes made in the specific version. - If msg is not empty string, show message box and returns. - """ - if msg != "": - QMessageBox.information(None, "Mergin", msg) - return - - dlg = DiffViewerDialog(version) - if dlg.diff_layers: - dlg.exec_() - else: - QMessageBox.information(None, "Mergin", "No changes to the current project layers for this version.") - - def set_mergin_client(self, mc): - self.mc = mc - - def on_qgis_project_changed(self): - self.model.clear() - self.project_path = mergin_project_local_path() - self.update_ui() diff --git a/Mergin/ui/ui_version_details_dialog.ui b/Mergin/ui/ui_version_details_dialog.ui deleted file mode 100644 index 864907b0..00000000 --- a/Mergin/ui/ui_version_details_dialog.ui +++ /dev/null @@ -1,147 +0,0 @@ - - - Dialog - - - - 0 - 0 - 401 - 335 - - - - Version Details - - - - - - Version - - - - - - - true - - - - - - - Author - - - - - - - true - - - - - - - Project size - - - - - - - true - - - - - - - Created - - - - - - - true - - - - - - - User Agent - - - - - - - true - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::NoSelection - - - true - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Close - - - - - - - - - buttonBox - accepted() - Dialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - Dialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/Mergin/ui/ui_draft_versions_viewer.ui b/Mergin/ui/ui_versions_viewer.ui similarity index 100% rename from Mergin/ui/ui_draft_versions_viewer.ui rename to Mergin/ui/ui_versions_viewer.ui diff --git a/Mergin/version_details_dialog.py b/Mergin/version_details_dialog.py deleted file mode 100644 index 730a8210..00000000 --- a/Mergin/version_details_dialog.py +++ /dev/null @@ -1,77 +0,0 @@ -import os - -from qgis.PyQt import uic -from qgis.PyQt.QtWidgets import QDialog -from qgis.PyQt.QtGui import QStandardItemModel, QStandardItem, QIcon - -from qgis.gui import QgsGui -from qgis.core import QgsMessageLog - -from .utils import is_versioned_file, icon_path, format_datetime - -from .mergin.utils import bytes_to_human_size - -ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_version_details_dialog.ui") - - -class VersionDetailsDialog(QDialog): - icons = { - "added": "plus.svg", - "removed": "trash.svg", - "updated": "pencil.svg", - "renamed": "pencil.svg", - "table": "table.svg", - } - - def __init__(self, version_details, parent=None): - QDialog.__init__(self, parent) - self.ui = uic.loadUi(ui_file, self) - QgsGui.instance().enableAutoGeometryRestore(self) - - self.version_details = version_details - - self.model = QStandardItemModel() - self.model.setHorizontalHeaderLabels(["Details"]) - self.tree_details.setModel(self.model) - self.populate_details() - self.tree_details.expandAll() - - def populate_details(self): - self.edit_version.setText(self.version_details["name"]) - self.edit_author.setText(self.version_details["author"]) - self.edit_project_size.setText(bytes_to_human_size(self.version_details["project_size"])) - self.edit_created.setText(format_datetime(self.version_details["created"])) - self.edit_user_agent.setText(self.version_details["user_agent"]) - - root_item = QStandardItem(f"Changes in version {self.version_details['name']}") - self.model.appendRow(root_item) - for category in self.version_details["changes"]: - for item in self.version_details["changes"][category]: - path = item["path"] - item = self._get_icon_item(category, path) - if is_versioned_file(path): - if path in self.version_details["changesets"]: - for sub_item in self._versioned_file_summary_items( - self.version_details["changesets"][path]["summary"] - ): - item.appendRow(sub_item) - root_item.appendRow(item) - - def _get_icon_item(self, key, text): - path = icon_path(self.icons[key]) - item = QStandardItem(text) - item.setIcon(QIcon(path)) - return item - - def _versioned_file_summary_items(self, summary): - items = [] - for s in summary: - table_name_item = self._get_icon_item("table", s["table"]) - for row in self._table_summary_items(s): - table_name_item.appendRow(row) - items.append(table_name_item) - - return items - - def _table_summary_items(self, summary): - return [QStandardItem("{}: {}".format(k, summary[k])) for k in summary if k != "table"] diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 2b231067..be5f6761 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -53,7 +53,7 @@ from .diff import make_version_changes_layers, icon_for_layer -ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_draft_versions_viewer.ui") +ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_versions_viewer.ui") class VersionsTableModel(QAbstractTableModel): From 625fa816224bef93123d783a7c99060da96a3abf Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 21 Oct 2024 14:26:52 +0200 Subject: [PATCH 28/84] version parse --- Mergin/ui/ui_versions_viewer.ui | 4 ++-- Mergin/utils.py | 15 +++++++++++++++ Mergin/version_viewer_dialog.py | 3 ++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Mergin/ui/ui_versions_viewer.ui b/Mergin/ui/ui_versions_viewer.ui index 5ae806ba..c64a2639 100644 --- a/Mergin/ui/ui_versions_viewer.ui +++ b/Mergin/ui/ui_versions_viewer.ui @@ -259,7 +259,7 @@ - User Agent + Created with: @@ -293,7 +293,7 @@ - Created + Created at diff --git a/Mergin/utils.py b/Mergin/utils.py index 2123f169..de4a08ff 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1550,3 +1550,18 @@ def format_datetime(date_string): """Formats datetime string returned by the server into human-readable format""" dt = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%SZ") return dt.strftime("%a, %d %b %Y %H:%M:%S GMT") + +def parse_user_agent(user_agent:str)->str: + user_agent = user_agent.split() + #Does the python-api-client have a specific user_agent ? + if len(user_agent) == 4: #Plugin + _, plugin_version, qgis_version, platform = user_agent + plugin_version = plugin_version.replace("Plugin/", "") + qgis_version = qgis_version.replace("QGIS/", "") + platform = platform.strip("()") + return f"MerginMaps plugin version {plugin_version} and QGIS version {qgis_version} on {platform}" + elif len(user_agent) == 2: + mobile_version, platform = user_agent + mobile_version = mobile_version.replace("Input/", "") + platform = platform.strip("()") + return f"Mobile app version {mobile_version} on {platform}" \ No newline at end of file diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index be5f6761..495bc2a0 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -44,6 +44,7 @@ is_versioned_file, icon_path, format_datetime, + parse_user_agent ) from .mergin.merginproject import MerginProject @@ -446,7 +447,7 @@ def current_version_changed(self, current_index, previous_index): def populate_details(self): self.edit_project_size.setText(bytes_to_human_size(self.version_details["project_size"])) self.edit_created.setText(format_datetime(self.version_details["created"])) - self.edit_user_agent.setText(self.version_details["user_agent"]) + self.edit_user_agent.setText(parse_user_agent(self.version_details["user_agent"])) self.model_detail.clear() root_item = QStandardItem(f"Changes in version {self.version_details['name']}") From 2ec26ff99d4cb95e5bdb2899a960fe2a8c59e0fe Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 21 Oct 2024 14:27:06 +0200 Subject: [PATCH 29/84] Fix thread issue --- Mergin/version_viewer_dialog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 495bc2a0..e9f5409f 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -199,6 +199,9 @@ def run(self): if not os.path.exists(full_gpkg): self.mc.download_file(self.mp.dir, f["path"], full_gpkg, f"v{self.version}") + if self.isInterruptionRequested(): + self.quit() + self.finished.emit("") From a3c1d23b88da1878cb8f5f30267fb0636000e11f Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 22 Oct 2024 12:17:19 +0200 Subject: [PATCH 30/84] Fix some version not working --- Mergin/version_viewer_dialog.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index e9f5409f..108c4554 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -177,18 +177,27 @@ def __init__(self, mc, mp, version): self.version = version def run(self): - info = self.mc.project_info(self.mp.project_full_name(), version=f"v{self.version}") - files = [f for f in info["files"] if is_versioned_file(f["path"])] - if not files: + version_info = self.mc.project_version_info(self.mp.project_id(), version=f"v{self.version}") + + files_updated = version_info["changes"]["updated"] + + #if file not in project_version_info # skip as well + if not version_info["changesets"]: + self.finished.emit("This version does not contain changes in the project layers.") + return + + files_updated = [f for f in files_updated if is_versioned_file(f["path"])] + + if not files_updated: self.finished.emit("This version does not contain changes in the project layers.") return - has_history = any("diff" in f for f in files) + has_history = any("diff" in f for f in files_updated) if not has_history: self.finished.emit("This version does not contain changes in the project layers.") return - for f in files: + for f in files_updated: if self.isInterruptionRequested(): return From 111fd9239e08c07e94f5ea486645ffb4d1dcee8f Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 22 Oct 2024 13:35:31 +0200 Subject: [PATCH 31/84] Add loading --- Mergin/ui/ui_versions_viewer.ui | 2 +- Mergin/version_viewer_dialog.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Mergin/ui/ui_versions_viewer.ui b/Mergin/ui/ui_versions_viewer.ui index c64a2639..63af72c6 100644 --- a/Mergin/ui/ui_versions_viewer.ui +++ b/Mergin/ui/ui_versions_viewer.ui @@ -219,7 +219,7 @@ - + 110 diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 108c4554..b0bc750f 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -187,7 +187,7 @@ def run(self): return files_updated = [f for f in files_updated if is_versioned_file(f["path"])] - + if not files_updated: self.finished.emit("This version does not contain changes in the project layers.") return @@ -443,6 +443,10 @@ def current_version_changed(self, current_index, previous_index): self.layer_list.clear() if not os.path.exists(os.path.join(self.project_path, ".mergin", ".cache", f"v{version}")): + + self.stackedWidget.setCurrentIndex(1) + self.label_info.setText("Loading version info…") + if self.diff_downloader and self.diff_downloader.isRunning(): self.diff_downloader.requestInterruption() @@ -531,6 +535,7 @@ def show_version_changes(self, version): else: self.toolbar.setEnabled(False) self.stackedWidget.setCurrentIndex(1) + self.label_info.setText("No visual changes") self.tabWidget.setCurrentIndex(1) self.tabWidget.setTabEnabled(0, False) From 363ea0c2889b41db3f374e969941e53db1e9f7a1 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 22 Oct 2024 16:04:49 +0200 Subject: [PATCH 32/84] Fix fetching last version erroneously --- Mergin/utils.py | 9 +++++---- Mergin/version_viewer_dialog.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Mergin/utils.py b/Mergin/utils.py index de4a08ff..6f3f2c3c 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1551,10 +1551,11 @@ def format_datetime(date_string): dt = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%SZ") return dt.strftime("%a, %d %b %Y %H:%M:%S GMT") -def parse_user_agent(user_agent:str)->str: + +def parse_user_agent(user_agent: str) -> str: user_agent = user_agent.split() - #Does the python-api-client have a specific user_agent ? - if len(user_agent) == 4: #Plugin + # Does the python-api-client have a specific user_agent ? + if len(user_agent) == 4: # Plugin _, plugin_version, qgis_version, platform = user_agent plugin_version = plugin_version.replace("Plugin/", "") qgis_version = qgis_version.replace("QGIS/", "") @@ -1564,4 +1565,4 @@ def parse_user_agent(user_agent:str)->str: mobile_version, platform = user_agent mobile_version = mobile_version.replace("Input/", "") platform = platform.strip("()") - return f"Mobile app version {mobile_version} on {platform}" \ No newline at end of file + return f"Mobile app version {mobile_version} on {platform}" diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index b0bc750f..a1707071 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -44,7 +44,7 @@ is_versioned_file, icon_path, format_datetime, - parse_user_agent + parse_user_agent, ) from .mergin.merginproject import MerginProject @@ -181,7 +181,7 @@ def run(self): files_updated = version_info["changes"]["updated"] - #if file not in project_version_info # skip as well + # if file not in project_version_info # skip as well if not version_info["changesets"]: self.finished.emit("This version does not contain changes in the project layers.") return @@ -416,6 +416,11 @@ def fetch_from_server(self): self.fetcher.start() def on_scrollbar_changed(self, value): + + if self.versionModel.oldest_version() == 1: + # No version left to fetch + return + if self.ui.history_treeview.verticalScrollBar().maximum() <= value: self.fetch_from_server() @@ -539,7 +544,7 @@ def show_version_changes(self, version): self.tabWidget.setCurrentIndex(1) self.tabWidget.setTabEnabled(0, False) - def collect_layers(self, checked): + def collect_layers(self, checked: bool): if checked: layers = iface.mapCanvas().layers() else: @@ -550,7 +555,7 @@ def collect_layers(self, checked): return layers - def diff_layer_changed(self, index): + def diff_layer_changed(self, index: int): if index > len(self.diff_layers): return From 180400224800b56e11780fc21e427750801b4852 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 24 Oct 2024 14:12:28 +0200 Subject: [PATCH 33/84] Fix unconnect action to method --- Mergin/version_viewer_dialog.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index a1707071..320aecb0 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -322,11 +322,15 @@ def __init__(self, mc, parent=None): self.toolbar.addSeparator() self.zoom_full_action = QAction(QgsApplication.getThemeIcon("/mActionZoomFullExtent.svg"), "Zoom Full", self) + self.zoom_full_action.triggered.connect(self.zoom_full) + self.toolbar.addAction(self.zoom_full_action) self.zoom_selected_action = QAction( QgsApplication.getThemeIcon("/mActionZoomToSelected.svg"), "Zoom To Selection", self ) + self.zoom_selected_action.triggered.connect(self.zoom_selected) + self.toolbar.addAction(self.zoom_selected_action) btn_add_changes = QPushButton("Add to project") @@ -591,3 +595,13 @@ def add_current_to_project(self): def add_all_to_project(self): for layer in self.diff_layers: QgsProject.instance().addMapLayer(layer) + + def zoom_full(self): + if self.current_diff: + self.map_canvas.setExtent(self.current_diff.extent()) + self.map_canvas.refresh() + + def zoom_selected(self): + if self.current_diff: + self.map_canvas.zoomToSelected([self.current_diff]) + self.map_canvas.refresh() From 6ded9f19f75b79d69d3bb3b024dad10339a1eef0 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 24 Oct 2024 14:15:07 +0200 Subject: [PATCH 34/84] Change default info text --- Mergin/ui/ui_versions_viewer.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mergin/ui/ui_versions_viewer.ui b/Mergin/ui/ui_versions_viewer.ui index 63af72c6..c44f4e0b 100644 --- a/Mergin/ui/ui_versions_viewer.ui +++ b/Mergin/ui/ui_versions_viewer.ui @@ -234,7 +234,7 @@ - No visual changes + Loading version info… From 5fd2796d28e731663230649409ad38a58be1fd48 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 24 Oct 2024 14:15:29 +0200 Subject: [PATCH 35/84] Remove superflous import --- Mergin/version_viewer_dialog.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 320aecb0..88a73acf 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -17,29 +17,14 @@ from qgis.utils import iface from qgis.core import QgsProject, QgsMessageLog, QgsApplication, QgsFeatureRequest, QgsVectorLayerCache -from qgis.gui import QgsGui, QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel +from qgis.gui import QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel from .utils import ( - ServerType, ClientError, - LoginError, - InvalidProject, - check_mergin_subdirs, - create_mergin_client, - find_qgis_files, - get_mergin_auth, icon_path, - mm_symbol_path, - is_number, - login_error_message, mergin_project_local_path, PROJS_PER_PAGE, - remove_project_variables, - same_dir, - unhandled_exception_message, - unsaved_project_check, - UnsavedChangesStrategy, contextual_date, is_versioned_file, icon_path, From 6ac521165c6ddb986b9d33473a9f1dfbb77b16a2 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 24 Oct 2024 14:18:39 +0200 Subject: [PATCH 36/84] Remove not used methods --- Mergin/version_viewer_dialog.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 88a73acf..afe7db30 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -126,16 +126,6 @@ def prepend_versions(self, versions): self.versions.extendleft(versions) self.layoutChanged.emit() - def canFetchMore(self, parent: QModelIndex) -> bool: - # Fetch while we are not the the first version - return self.oldest_version() == None or self.oldest_version() >= 1 - - def fetchMore(self, parent: QModelIndex) -> None: - pass - # emit - # fetcher = VersionsFetcher(self.mc,self.mp.project_full_name(), self.model) - # fetcher.finished.connect(lambda versions: self.model.add_versions(versions)) - # fetcher.start() def item_from_index(self, index): return self.versions[index.row()] From aa09ed30fb747606246fb340d89a3044e29e0238 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 24 Oct 2024 14:50:25 +0200 Subject: [PATCH 37/84] Remove dependancy to model variable --- Mergin/version_viewer_dialog.py | 44 ++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index afe7db30..ce63ec12 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -2,7 +2,14 @@ import os from qgis.PyQt import uic, QtCore -from qgis.PyQt.QtWidgets import QDialog, QAction, QListWidgetItem, QPushButton, QMenu, QMessageBox +from qgis.PyQt.QtWidgets import ( + QDialog, + QAction, + QListWidgetItem, + QPushButton, + QMenu, + QMessageBox, +) from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont, QColor from qgis.PyQt.QtCore import ( QStringListModel, @@ -16,7 +23,13 @@ ) from qgis.utils import iface -from qgis.core import QgsProject, QgsMessageLog, QgsApplication, QgsFeatureRequest, QgsVectorLayerCache +from qgis.core import ( + QgsProject, + QgsMessageLog, + QgsApplication, + QgsFeatureRequest, + QgsVectorLayerCache, +) from qgis.gui import QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel @@ -126,7 +139,6 @@ def prepend_versions(self, versions): self.versions.extendleft(versions) self.layoutChanged.emit() - def item_from_index(self, index): return self.versions[index.row()] @@ -193,11 +205,20 @@ class VersionsFetcher(QThread): finished = pyqtSignal(list) - def __init__(self, mc: MerginClient, project_name, model: VersionsTableModel, is_sync=False): + def __init__( + self, + mc: MerginClient, + project_name, + oldest_version: int | None, + latest_version: int | None, + is_sync=False, + ): super(VersionsFetcher, self).__init__() self.mc = mc self.project_name = project_name - self.model = model + + self.oldest_version = oldest_version + self.latest_version = latest_version self.is_sync = is_sync @@ -214,12 +235,12 @@ def run(self): def fetch_previous(self): - if len(self.model.versions) == 0: + if self.oldest_version == None: # initial fetch info = self.mc.project_info(self.project_name) to = int_version(info["version"]) else: - to = self.model.oldest_version() + to = self.oldest_version since = to - 100 if since < 0: since = 1 @@ -235,7 +256,7 @@ def fetch_sync_history(self): info = self.mc.project_info(self.project_name) latest_server = int_version(info["version"]) - since = self.model.latest_version() + since = self.latest_version versions = self.mc.project_versions(self.project_name, since=since, to=latest_server) versions.pop() # Remove the last as we already have it @@ -389,7 +410,12 @@ def fetch_from_server(self): # Only fetching when previous is finshed return - self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.versionModel) + self.fetcher = VersionsFetcher( + self.mc, + self.mp.project_full_name(), + self.versionModel.oldest_version(), + self.versionModel.latest_version(), + ) self.fetcher.finished.connect(lambda versions: self.versionModel.add_versions(versions)) self.fetcher.finished.connect(lambda versions: self.select_latest()) self.fetcher.start() From f22177a1f6430920a93c07693fe0e937c92c5b2c Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 24 Oct 2024 14:55:12 +0200 Subject: [PATCH 38/84] Add licence header --- Mergin/version_viewer_dialog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index ce63ec12..c26eb6cf 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -1,3 +1,6 @@ +# GPLv3 license +# Copyright Lutra Consulting Limited + from collections import deque import os From aa55525eb8169406035b79234db1ecf6b5d6f8a4 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 29 Oct 2024 13:35:22 +0100 Subject: [PATCH 39/84] Fix map layers not updated in some conditions --- Mergin/version_viewer_dialog.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index c26eb6cf..ed42e9e4 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -469,10 +469,6 @@ def current_version_changed(self, current_index, previous_index): else: self.show_version_changes(version) - self.update_canvas(self.diff_layers) - - return self.versionModel.data(current_index, VersionsTableModel.VERSION) - def populate_details(self): self.edit_project_size.setText(bytes_to_human_size(self.version_details["project_size"])) self.edit_created.setText(format_datetime(self.version_details["created"])) @@ -545,6 +541,8 @@ def show_version_changes(self, version): self.stackedWidget.setCurrentIndex(0) self.tabWidget.setCurrentIndex(0) self.tabWidget.setTabEnabled(0, True) + layers = self.collect_layers(self.toggle_layers_action.isChecked()) + self.update_canvas(layers) else: self.toolbar.setEnabled(False) self.stackedWidget.setCurrentIndex(1) From a5c39ef9510e8c3e231dbfff3e86fabf780099bc Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 29 Oct 2024 17:01:05 +0100 Subject: [PATCH 40/84] Set changeset details not editable --- Mergin/version_viewer_dialog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index ed42e9e4..66011343 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -12,6 +12,7 @@ QPushButton, QMenu, QMessageBox, + QAbstractItemView, ) from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont, QColor from qgis.PyQt.QtCore import ( @@ -363,6 +364,8 @@ def __init__(self, mc, parent=None): } self.model_detail = QStandardItemModel() self.model_detail.setHorizontalHeaderLabels(["Details"]) + + self.details_treeview.setEditTriggers(QAbstractItemView.NoEditTriggers) self.details_treeview.setModel(self.model_detail) self.versionModel.current_version = self.mp.version() From a02c2342c7c8f59124025dcffddb299973e87e61 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 30 Oct 2024 10:29:31 +0100 Subject: [PATCH 41/84] Fix elapsed time not working because of difference between local and gmt datetime --- Mergin/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Mergin/utils.py b/Mergin/utils.py index 567c23b7..cfe1c455 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1,5 +1,5 @@ import shutil -from datetime import datetime, timezone +from datetime import datetime, timezone, tzinfo from enum import Enum from urllib.error import URLError, HTTPError import configparser @@ -1518,12 +1518,13 @@ def check_mergin_subdirs(directory): return False -def contextual_date(date_string, start_date=None): +def contextual_date(date_string): """Converts datetime string returned by the server into contextual duration string, e.g. 'N hours/days/month ago' """ dt = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%SZ") - now = datetime.now() if start_date is None else datetime.strptime(start_date, "%Y-%m-%dT%H:%M:%SZ") + dt = dt.replace(tzinfo=timezone.utc) + now = datetime.now(timezone.utc) delta = now - dt if delta.days > 365: years = now.year - dt.year - ((now.month, now.day) < (dt.month, dt.day)) From af0d09198746883e2d6914710018c93b5e27f464 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 30 Oct 2024 12:48:51 +0100 Subject: [PATCH 42/84] Display text as well for button --- Mergin/version_viewer_dialog.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 66011343..75d5cd46 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -13,6 +13,7 @@ QMenu, QMessageBox, QAbstractItemView, + QToolButton ) from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont, QColor from qgis.PyQt.QtCore import ( @@ -317,8 +318,14 @@ def __init__(self, mc, parent=None): self.toggle_layers_action.setCheckable(True) self.toggle_layers_action.setChecked(True) self.toggle_layers_action.toggled.connect(self.toggle_project_layers) - self.toolbar.addAction(self.toggle_layers_action) + #We use a ToolButton instead of simple action to dislay both icon AND text + self.toggle_layers_button = QToolButton() + self.toggle_layers_button.setDefaultAction(self.toggle_layers_action) + self.toggle_layers_button.setText("Show projecct layers") + self.toggle_layers_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.toolbar.addWidget(self.toggle_layers_button) + self.toolbar.addSeparator() self.zoom_full_action = QAction(QgsApplication.getThemeIcon("/mActionZoomFullExtent.svg"), "Zoom Full", self) From c7fb9b764889a962c35f1e4c8a8c4d8865493e21 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 30 Oct 2024 13:05:42 +0100 Subject: [PATCH 43/84] Remove unused fetching on sync --- Mergin/version_viewer_dialog.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 75d5cd46..1f71a0a4 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -216,7 +216,6 @@ def __init__( project_name, oldest_version: int | None, latest_version: int | None, - is_sync=False, ): super(VersionsFetcher, self).__init__() self.mc = mc @@ -225,16 +224,10 @@ def __init__( self.oldest_version = oldest_version self.latest_version = latest_version - self.is_sync = is_sync - self.per_page = 50 # server limit def run(self): - - if not self.is_sync: - versions = self.fetch_previous() - else: - versions = self.fetch_sync_history() + versions = self.fetch_previous() self.finished.emit(versions) @@ -255,20 +248,6 @@ def fetch_previous(self): return versions - def fetch_sync_history(self): - - # determine latest - info = self.mc.project_info(self.project_name) - - latest_server = int_version(info["version"]) - since = self.latest_version - - versions = self.mc.project_versions(self.project_name, since=since, to=latest_server) - versions.pop() # Remove the last as we already have it - versions.reverse() - - return versions - class VersionViewerDialog(QDialog): """ From 8193a0f7c6f10d3381173594849f739d65bfee33 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 30 Oct 2024 14:18:03 +0100 Subject: [PATCH 44/84] Consider more user agent and simplify code --- Mergin/utils.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/Mergin/utils.py b/Mergin/utils.py index cfe1c455..9f9371f1 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1556,16 +1556,21 @@ def format_datetime(date_string): def parse_user_agent(user_agent: str) -> str: - user_agent = user_agent.split() - # Does the python-api-client have a specific user_agent ? - if len(user_agent) == 4: # Plugin - _, plugin_version, qgis_version, platform = user_agent - plugin_version = plugin_version.replace("Plugin/", "") - qgis_version = qgis_version.replace("QGIS/", "") - platform = platform.strip("()") - return f"MerginMaps plugin version {plugin_version} and QGIS version {qgis_version} on {platform}" - elif len(user_agent) == 2: - mobile_version, platform = user_agent - mobile_version = mobile_version.replace("Input/", "") - platform = platform.strip("()") - return f"Mobile app version {mobile_version} on {platform}" + browsers = ["Chrome", "Firefox", "Mozilla", "Opera", "Safari", "Webkit"] + if any([ browser in user_agent for browser in browsers]): + return "Web dashboard" + elif "Input" in user_agent: + return "Mobile app" + elif "Plugin" in user_agent: + return "QGIS plugin" + elif "DB-sync" in user_agent: + return "Synced from db-sync" + elif "work-packages" in user_agent: + return "Synced from Work Packages" + elif "media-sync" in user_agent: + return "Synced from Media Sync" + elif "Python-client" in user_agent: + return "Mergin Maps Python Client" + else: # For uncommon user agent we display user agent as is + return user_agent + \ No newline at end of file From cec17086ccbce7e04ff4f8ab4ff0ca97499ecc4f Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 4 Nov 2024 13:09:25 +0100 Subject: [PATCH 45/84] Simplify version fetcher --- Mergin/version_viewer_dialog.py | 80 +++++++++++---------------------- 1 file changed, 26 insertions(+), 54 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 1f71a0a4..98edebf4 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -3,6 +3,7 @@ from collections import deque import os +import math from qgis.PyQt import uic, QtCore from qgis.PyQt.QtWidgets import ( @@ -213,40 +214,33 @@ class VersionsFetcher(QThread): def __init__( self, mc: MerginClient, - project_name, - oldest_version: int | None, - latest_version: int | None, + project_path, + model : VersionsTableModel ): super(VersionsFetcher, self).__init__() self.mc = mc - self.project_name = project_name + self.project_path = project_path + self.model = model - self.oldest_version = oldest_version - self.latest_version = latest_version + self.current_page = 1 + self.per_page = 50 - self.per_page = 50 # server limit + version_count = self.mc.project_versions_count(self.project_path) + self.nb_page = math.ceil(version_count / self.per_page) def run(self): - versions = self.fetch_previous() + self.fetch_another_page() + + def has_more_page(self): + return self.current_page <= self.nb_page - self.finished.emit(versions) - - def fetch_previous(self): - - if self.oldest_version == None: - # initial fetch - info = self.mc.project_info(self.project_name) - to = int_version(info["version"]) - else: - to = self.oldest_version - since = to - 100 - if since < 0: - since = 1 - - versions = self.mc.project_versions(self.project_name, since=since, to=to) - versions.reverse() - - return versions + def fetch_another_page(self): + if self.has_more_page() == False: + return + versions = self.mc.project_versions_page(self.project_path, self.current_page, per_page=self.per_page, descending=True) + self.model.add_versions(versions) + + self.current_page += 1 class VersionViewerDialog(QDialog): @@ -264,14 +258,10 @@ def __init__(self, mc, parent=None): self.ui = uic.loadUi(ui_file, self) self.mc = mc - self.mp = None self.project_path = mergin_project_local_path() self.mp = MerginProject(self.project_path) - self.fetcher = None - self.diff_downloader = None - self.set_splitters_state() self.versionModel = VersionsTableModel() @@ -283,7 +273,10 @@ def __init__(self, mc, parent=None): self.has_selected_latest = False - self.fetch_from_server() + self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.versionModel) + self.diff_downloader = None + + self.fetcher.fetch_another_page() height = 30 self.toolbar.setMinimumHeight(height) @@ -401,35 +394,14 @@ def fetch_from_server(self): if self.fetcher and self.fetcher.isRunning(): # Only fetching when previous is finshed return - - self.fetcher = VersionsFetcher( - self.mc, - self.mp.project_full_name(), - self.versionModel.oldest_version(), - self.versionModel.latest_version(), - ) - self.fetcher.finished.connect(lambda versions: self.versionModel.add_versions(versions)) - self.fetcher.finished.connect(lambda versions: self.select_latest()) - self.fetcher.start() + else: + self.fetcher.start() def on_scrollbar_changed(self, value): - if self.versionModel.oldest_version() == 1: - # No version left to fetch - return - if self.ui.history_treeview.verticalScrollBar().maximum() <= value: self.fetch_from_server() - def select_latest(self): - # On open dialog select the latest version - if self.has_selected_latest and self.has_selected_latest == True: - return - - self.has_selected_latest = True - - index = self.versionModel.index(0, 0) - self.selectionModel.setCurrentIndex(index, QItemSelectionModel.Select | QItemSelectionModel.Rows) def current_version_changed(self, current_index, previous_index): # Update the ui when the selected version change From e1e02caa9b6f28c052020ddc04dd26fcba4e1b97 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 4 Nov 2024 13:10:03 +0100 Subject: [PATCH 46/84] Fix early cancel dialog --- Mergin/plugin.py | 2 +- Mergin/version_viewer_dialog.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Mergin/plugin.py b/Mergin/plugin.py index 42632605..3ed185e9 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -338,7 +338,7 @@ def configure_db_sync(self): def open_project_history_window(self): dlg = VersionViewerDialog(self.mc) - dlg.exec_() + dlg.exec() def show_no_workspaces_dialog(self): msg = ( diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 98edebf4..90f026c9 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -350,6 +350,7 @@ def __init__(self, mc, parent=None): self.versionModel.current_version = self.mp.version() def exec(self): + try: ws_id = self.mp.workspace_id() except ClientError as e: @@ -357,13 +358,15 @@ def exec(self): return # check if user has permissions - usage = self.mc.workspace_usage(ws_id) - if not usage["view_history"]["allowed"]: - QMessageBox.warning(None, "Permission Error", "The workspace does not allow to view project history.") - return - - self.reject() - return + try: + usage = self.mc.workspace_usage(ws_id) + if not usage["view_history"]["allowed"]: + QMessageBox.warning(None, "Permission Error", "The workspace does not allow to view project history.") + return + except ClientError: + # Some versions e.g CE, EE edition doesn't have + pass + super().exec() def closeEvent(self, event): self.save_splitters_state() From 082f58d9ab8872df2a58737332b2b1e1985bf687 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 4 Nov 2024 13:10:16 +0100 Subject: [PATCH 47/84] Add tooltip --- Mergin/version_viewer_dialog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 90f026c9..0d6a2579 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -437,6 +437,8 @@ def populate_details(self): self.edit_project_size.setText(bytes_to_human_size(self.version_details["project_size"])) self.edit_created.setText(format_datetime(self.version_details["created"])) self.edit_user_agent.setText(parse_user_agent(self.version_details["user_agent"])) + self.edit_user_agent.setToolTip(self.version_details["user_agent"]) + self.model_detail.clear() root_item = QStandardItem(f"Changes in version {self.version_details['name']}") From 44193ce653f1ad5b74c7e67833ffc8f55d98a314 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 4 Nov 2024 13:11:01 +0100 Subject: [PATCH 48/84] Format --- Mergin/utils.py | 5 ++--- Mergin/version_viewer_dialog.py | 35 ++++++++++++++------------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Mergin/utils.py b/Mergin/utils.py index 9f9371f1..ba27b39c 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1557,7 +1557,7 @@ def format_datetime(date_string): def parse_user_agent(user_agent: str) -> str: browsers = ["Chrome", "Firefox", "Mozilla", "Opera", "Safari", "Webkit"] - if any([ browser in user_agent for browser in browsers]): + if any([browser in user_agent for browser in browsers]): return "Web dashboard" elif "Input" in user_agent: return "Mobile app" @@ -1571,6 +1571,5 @@ def parse_user_agent(user_agent: str) -> str: return "Synced from Media Sync" elif "Python-client" in user_agent: return "Mergin Maps Python Client" - else: # For uncommon user agent we display user agent as is + else: # For uncommon user agent we display user agent as is return user_agent - \ No newline at end of file diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 0d6a2579..3185f338 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -14,7 +14,7 @@ QMenu, QMessageBox, QAbstractItemView, - QToolButton + QToolButton, ) from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel, QIcon, QFont, QColor from qgis.PyQt.QtCore import ( @@ -211,35 +211,32 @@ class VersionsFetcher(QThread): finished = pyqtSignal(list) - def __init__( - self, - mc: MerginClient, - project_path, - model : VersionsTableModel - ): + def __init__(self, mc: MerginClient, project_path, model: VersionsTableModel): super(VersionsFetcher, self).__init__() self.mc = mc self.project_path = project_path self.model = model - self.current_page = 1 - self.per_page = 50 + self.current_page = 1 + self.per_page = 50 version_count = self.mc.project_versions_count(self.project_path) self.nb_page = math.ceil(version_count / self.per_page) def run(self): self.fetch_another_page() - + def has_more_page(self): return self.current_page <= self.nb_page def fetch_another_page(self): if self.has_more_page() == False: return - versions = self.mc.project_versions_page(self.project_path, self.current_page, per_page=self.per_page, descending=True) + versions = self.mc.project_versions_page( + self.project_path, self.current_page, per_page=self.per_page, descending=True + ) self.model.add_versions(versions) - + self.current_page += 1 @@ -291,13 +288,13 @@ def __init__(self, mc, parent=None): self.toggle_layers_action.setChecked(True) self.toggle_layers_action.toggled.connect(self.toggle_project_layers) - #We use a ToolButton instead of simple action to dislay both icon AND text + # We use a ToolButton instead of simple action to dislay both icon AND text self.toggle_layers_button = QToolButton() self.toggle_layers_button.setDefaultAction(self.toggle_layers_action) self.toggle_layers_button.setText("Show projecct layers") self.toggle_layers_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.toolbar.addWidget(self.toggle_layers_button) - + self.toolbar.addSeparator() self.zoom_full_action = QAction(QgsApplication.getThemeIcon("/mActionZoomFullExtent.svg"), "Zoom Full", self) @@ -350,7 +347,7 @@ def __init__(self, mc, parent=None): self.versionModel.current_version = self.mp.version() def exec(self): - + try: ws_id = self.mp.workspace_id() except ClientError as e: @@ -358,13 +355,13 @@ def exec(self): return # check if user has permissions - try: + try: usage = self.mc.workspace_usage(ws_id) if not usage["view_history"]["allowed"]: QMessageBox.warning(None, "Permission Error", "The workspace does not allow to view project history.") return - except ClientError: - # Some versions e.g CE, EE edition doesn't have + except ClientError: + # Some versions e.g CE, EE edition doesn't have pass super().exec() @@ -405,7 +402,6 @@ def on_scrollbar_changed(self, value): if self.ui.history_treeview.verticalScrollBar().maximum() <= value: self.fetch_from_server() - def current_version_changed(self, current_index, previous_index): # Update the ui when the selected version change item = self.versionModel.item_from_index(current_index) @@ -439,7 +435,6 @@ def populate_details(self): self.edit_user_agent.setText(parse_user_agent(self.version_details["user_agent"])) self.edit_user_agent.setToolTip(self.version_details["user_agent"]) - self.model_detail.clear() root_item = QStandardItem(f"Changes in version {self.version_details['name']}") self.model_detail.appendRow(root_item) From da630883e242aa0898f60c6c61e1b4aa53ac84fb Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 4 Nov 2024 13:17:15 +0100 Subject: [PATCH 49/84] change contextual_date for older than one year --- Mergin/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mergin/utils.py b/Mergin/utils.py index ba27b39c..07febfc4 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1527,8 +1527,8 @@ def contextual_date(date_string): now = datetime.now(timezone.utc) delta = now - dt if delta.days > 365: - years = now.year - dt.year - ((now.month, now.day) < (dt.month, dt.day)) - return f"{years} {'years' if years > 1 else 'year'} ago" + # return the date value for version older than one year + return dt.strftime("%Y-%m-%d") elif delta.days > 31: months = int(delta.days // 30.436875) return f"{months} {'months' if months > 1 else 'month'} ago" From 8f8fdbe25a429377c582f7de18f587cc6ce6bf4d Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 4 Nov 2024 13:21:20 +0100 Subject: [PATCH 50/84] More sales friendly message --- Mergin/version_viewer_dialog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 3185f338..75504c35 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -358,7 +358,9 @@ def exec(self): try: usage = self.mc.workspace_usage(ws_id) if not usage["view_history"]["allowed"]: - QMessageBox.warning(None, "Permission Error", "The workspace does not allow to view project history.") + QMessageBox.warning( + None, "Upgrade required", "To view the project history, please upgrade your subscription plan." + ) return except ClientError: # Some versions e.g CE, EE edition doesn't have From b7666212310c1a8f05cc37086c865bccaffd5850 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 4 Nov 2024 13:24:19 +0100 Subject: [PATCH 51/84] Move function to utils --- Mergin/diff.py | 16 +--------------- Mergin/diff_dialog.py | 4 ++-- Mergin/utils.py | 17 ++++++++++++++++- Mergin/version_viewer_dialog.py | 3 ++- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Mergin/diff.py b/Mergin/diff.py index a548faaf..162a297b 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -7,7 +7,7 @@ from qgis.PyQt.QtCore import QVariant -from qgis.PyQt.QtGui import QColor, QIcon +from qgis.PyQt.QtGui import QColor from qgis.core import ( QgsApplication, @@ -530,17 +530,3 @@ def style_diff_layer(layer, schema_table): ) r = QgsRuleBasedRenderer(root_rule) layer.setRenderer(r) - - -def icon_for_layer(layer) -> QIcon: - geom_type = layer.geometryType() - if geom_type == QgsWkbTypes.PointGeometry: - return QgsApplication.getThemeIcon("/mIconPointLayer.svg") - elif geom_type == QgsWkbTypes.LineGeometry: - return QgsApplication.getThemeIcon("/mIconLineLayer.svg") - elif geom_type == QgsWkbTypes.PolygonGeometry: - return QgsApplication.getThemeIcon("/mIconPolygonLayer.svg") - elif geom_type == QgsWkbTypes.UnknownGeometry: - return QgsApplication.getThemeIcon("/mIconGeometryCollectionLayer.svg") - else: - return QgsApplication.getThemeIcon("/mIconTableLayer.svg") diff --git a/Mergin/diff_dialog.py b/Mergin/diff_dialog.py index 6321548b..91014b2a 100644 --- a/Mergin/diff_dialog.py +++ b/Mergin/diff_dialog.py @@ -18,8 +18,8 @@ from qgis.utils import iface, OverrideCursor from .mergin.merginproject import MerginProject -from .diff import make_local_changes_layer, make_version_changes_layers, icon_for_layer -from .utils import icon_path +from .diff import make_local_changes_layer, make_version_changes_layers +from .utils import icon_path, icon_for_layer ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_diff_viewer_dialog.ui") diff --git a/Mergin/utils.py b/Mergin/utils.py index 07febfc4..4299e55c 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -16,7 +16,7 @@ from qgis.PyQt.QtCore import QSettings, QVariant from qgis.PyQt.QtWidgets import QMessageBox, QFileDialog -from qgis.PyQt.QtGui import QPalette, QColor +from qgis.PyQt.QtGui import QPalette, QColor, QIcon from qgis.core import ( NULL, Qgis, @@ -1573,3 +1573,18 @@ def parse_user_agent(user_agent: str) -> str: return "Mergin Maps Python Client" else: # For uncommon user agent we display user agent as is return user_agent + + +def icon_for_layer(layer) -> QIcon: + # Used in diff viewer and history viewer + geom_type = layer.geometryType() + if geom_type == QgsWkbTypes.PointGeometry: + return QgsApplication.getThemeIcon("/mIconPointLayer.svg") + elif geom_type == QgsWkbTypes.LineGeometry: + return QgsApplication.getThemeIcon("/mIconLineLayer.svg") + elif geom_type == QgsWkbTypes.PolygonGeometry: + return QgsApplication.getThemeIcon("/mIconPolygonLayer.svg") + elif geom_type == QgsWkbTypes.UnknownGeometry: + return QgsApplication.getThemeIcon("/mIconGeometryCollectionLayer.svg") + else: + return QgsApplication.getThemeIcon("/mIconTableLayer.svg") diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 75504c35..1bde6d69 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -49,13 +49,14 @@ icon_path, format_datetime, parse_user_agent, + icon_for_layer, ) from .mergin.merginproject import MerginProject from .mergin.utils import bytes_to_human_size, int_version from .mergin import MerginClient -from .diff import make_version_changes_layers, icon_for_layer +from .diff import make_version_changes_layers ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_versions_viewer.ui") From 96642ce7de5f879400d52bdbf370fa5be8bc1c4e Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 5 Nov 2024 10:16:16 +0100 Subject: [PATCH 52/84] Fix root_dir parameter not existing in older python version --- Mergin/diff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mergin/diff.py b/Mergin/diff.py index 162a297b..81d6f6c4 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -330,7 +330,7 @@ def make_version_changes_layers(project_path, version): layers = [] version_dir = os.path.join(project_path, ".mergin", ".cache", f"v{version}") - for f in glob.iglob("*.gpkg", root_dir=version_dir): + for f in glob.iglob(f"{version_dir}/*.gpkg"): gpkg_file = os.path.join(version_dir, f) schema_file = gpkg_file + "-schema.json" if not os.path.exists(schema_file): @@ -377,7 +377,7 @@ def make_version_changes_layers(project_path, version): def find_changeset_file(file_name, version_dir): """Returns path to the diff file for the given version file""" - for f in glob.iglob("*.gpkg-diff*", root_dir=version_dir): + for f in glob.iglob(f"{version_dir}/*.gpkg-diff*"): if f.startswith(file_name): return os.path.join(version_dir, f) return None From 329aa010d215613373fccced2c45cc9055b2e535 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 5 Nov 2024 11:07:06 +0100 Subject: [PATCH 53/84] Typo --- Mergin/version_viewer_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 1bde6d69..bafeca24 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -292,7 +292,7 @@ def __init__(self, mc, parent=None): # We use a ToolButton instead of simple action to dislay both icon AND text self.toggle_layers_button = QToolButton() self.toggle_layers_button.setDefaultAction(self.toggle_layers_action) - self.toggle_layers_button.setText("Show projecct layers") + self.toggle_layers_button.setText("Show project layers") self.toggle_layers_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.toolbar.addWidget(self.toggle_layers_button) From 363b054455117bc41f3b10736a0e0747d544c933 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 6 Nov 2024 09:21:48 +0100 Subject: [PATCH 54/84] Change window title dynamically --- Mergin/ui/ui_versions_viewer.ui | 2 +- Mergin/version_viewer_dialog.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Mergin/ui/ui_versions_viewer.ui b/Mergin/ui/ui_versions_viewer.ui index c44f4e0b..ce612167 100644 --- a/Mergin/ui/ui_versions_viewer.ui +++ b/Mergin/ui/ui_versions_viewer.ui @@ -11,7 +11,7 @@ - Changes Viewer + Version Viewer diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index bafeca24..4f6446ae 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -411,6 +411,8 @@ def current_version_changed(self, current_index, previous_index): version_name = item["name"] version = int_version(item["name"]) + self.setWindowTitle(f"Version Viewer | {version_name}") + self.version_details = self.mc.project_version_info(self.mp.project_id(), version_name) self.populate_details() self.details_treeview.expandAll() From fb57936af428336d9e9528a0bb7fa8d32f657a33 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 6 Nov 2024 09:22:18 +0100 Subject: [PATCH 55/84] Remove superflous information in layer name --- Mergin/diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mergin/diff.py b/Mergin/diff.py index 81d6f6c4..05d38de0 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -361,7 +361,7 @@ def make_version_changes_layers(project_path, version): continue uri = f"{geom_type}?crs=epsg:{geom_crs}" if geom_crs else geom_type - vl = QgsVectorLayer(uri, f"[v{version} changes] {table_name}", "memory") + vl = QgsVectorLayer(uri, table_name, "memory") if not vl.isValid(): continue From c75e1f4185b7636ad02df9be4c7debe59c96d6f4 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 6 Nov 2024 09:22:40 +0100 Subject: [PATCH 56/84] Add information for local version --- Mergin/version_viewer_dialog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 4f6446ae..571443fb 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -107,6 +107,8 @@ def data(self, index, role=Qt.DisplayRole): idx = index.row() if role == Qt.DisplayRole: if index.column() == 0: + if self.versions[idx]["name"] == self.current_version: + return f'{self.versions[idx]["name"]} (local)' return self.versions[idx]["name"] if index.column() == 1: return self.versions[idx]["author"] From 9447fc3a051417409d11f2cbd2968ef57ee6cf41 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 6 Nov 2024 10:03:17 +0100 Subject: [PATCH 57/84] Revert name --- Mergin/ui/ui_versions_viewer.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mergin/ui/ui_versions_viewer.ui b/Mergin/ui/ui_versions_viewer.ui index ce612167..c44f4e0b 100644 --- a/Mergin/ui/ui_versions_viewer.ui +++ b/Mergin/ui/ui_versions_viewer.ui @@ -11,7 +11,7 @@ - Version Viewer + Changes Viewer From 69b0b53bffbe5425ac4293b288fef7fcdc9aa175 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 6 Nov 2024 12:32:25 +0100 Subject: [PATCH 58/84] * Only display background map --- Mergin/version_viewer_dialog.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 571443fb..629bf5e0 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -35,6 +35,11 @@ QgsApplication, QgsFeatureRequest, QgsVectorLayerCache, + + # Used to filter background map + QgsRasterLayer, + QgsTiledSceneLayer, + QgsVectorTileLayer ) from qgis.gui import QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel @@ -285,7 +290,7 @@ def __init__(self, mc, parent=None): self.history_control.setVisible(False) self.toggle_layers_action = QAction( - QgsApplication.getThemeIcon("/mActionAddLayer.svg"), "Toggle Project Layers", self + QgsApplication.getThemeIcon("/mActionAddLayer.svg"), "Hide background layers", self ) self.toggle_layers_action.setCheckable(True) self.toggle_layers_action.setChecked(True) @@ -294,7 +299,8 @@ def __init__(self, mc, parent=None): # We use a ToolButton instead of simple action to dislay both icon AND text self.toggle_layers_button = QToolButton() self.toggle_layers_button.setDefaultAction(self.toggle_layers_action) - self.toggle_layers_button.setText("Show project layers") + self.toggle_layers_button.setText("Show background layers") + self.toggle_layers_button.setToolTip("Toggle the display of background layer(Raster and tiles) in the current project") self.toggle_layers_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.toolbar.addWidget(self.toggle_layers_button) @@ -413,7 +419,7 @@ def current_version_changed(self, current_index, previous_index): version_name = item["name"] version = int_version(item["name"]) - self.setWindowTitle(f"Version Viewer | {version_name}") + self.setWindowTitle(f"Changes Viewer | {version_name}") self.version_details = self.mc.project_version_info(self.mp.project_id(), version_name) self.populate_details() @@ -477,6 +483,11 @@ def _table_summary_items(self, summary): return [QStandardItem("{}: {}".format(k, summary[k])) for k in summary if k != "table"] def toggle_project_layers(self, checked): + if checked: + self.toggle_layers_button.setText("Hide background layers") + else: + self.toggle_layers_button.setText("Show background layers") + layers = self.collect_layers(checked) self.update_canvas(layers) @@ -521,6 +532,10 @@ def show_version_changes(self, version): def collect_layers(self, checked: bool): if checked: layers = iface.mapCanvas().layers() + + #Filter only "Background" type + backgound_layer_types = [QgsRasterLayer, QgsVectorTileLayer, QgsTiledSceneLayer] + layers = [layer for layer in layers if type(layer) in backgound_layer_types] else: layers = [] From ad042bf4fa184b479ebac247701b37f2df51bb6f Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 6 Nov 2024 14:37:50 +0100 Subject: [PATCH 59/84] Fix extend changing when toggle background layers --- Mergin/version_viewer_dialog.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 629bf5e0..4d94e199 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -489,10 +489,19 @@ def toggle_project_layers(self, checked): self.toggle_layers_button.setText("Show background layers") layers = self.collect_layers(checked) - self.update_canvas(layers) + self.update_canvas_layers(layers) def update_canvas(self, layers): + self.update_canvas_layers(layers) + self.update_canvas_extend(layers) + + def update_canvas_layers(self, layers): self.map_canvas.setLayers(layers) + self.map_canvas.refresh() + + def update_canvas_extend(self, layers): + self.map_canvas.setDestinationCrs( QgsProject.instance().crs()) + if layers: self.map_canvas.setDestinationCrs(layers[0].crs()) extent = layers[0].extent() From 465e3675b6fc5a659a39aba99aa80dfc768422ea Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 6 Nov 2024 14:49:47 +0100 Subject: [PATCH 60/84] Revert "Fix root_dir parameter not existing in older python version" This reverts commit 96642ce7de5f879400d52bdbf370fa5be8bc1c4e. --- Mergin/diff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mergin/diff.py b/Mergin/diff.py index 05d38de0..b8a0db06 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -330,7 +330,7 @@ def make_version_changes_layers(project_path, version): layers = [] version_dir = os.path.join(project_path, ".mergin", ".cache", f"v{version}") - for f in glob.iglob(f"{version_dir}/*.gpkg"): + for f in glob.iglob("*.gpkg", root_dir=version_dir): gpkg_file = os.path.join(version_dir, f) schema_file = gpkg_file + "-schema.json" if not os.path.exists(schema_file): @@ -377,7 +377,7 @@ def make_version_changes_layers(project_path, version): def find_changeset_file(file_name, version_dir): """Returns path to the diff file for the given version file""" - for f in glob.iglob(f"{version_dir}/*.gpkg-diff*"): + for f in glob.iglob("*.gpkg-diff*", root_dir=version_dir): if f.startswith(file_name): return os.path.join(version_dir, f) return None From a10653fa5120a45617c8a8cae8e4c2dca3a9f5f2 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 6 Nov 2024 16:09:32 +0100 Subject: [PATCH 61/84] cleanup rename variable --- Mergin/version_viewer_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 4d94e199..c6e8d7c2 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -543,8 +543,8 @@ def collect_layers(self, checked: bool): layers = iface.mapCanvas().layers() #Filter only "Background" type - backgound_layer_types = [QgsRasterLayer, QgsVectorTileLayer, QgsTiledSceneLayer] - layers = [layer for layer in layers if type(layer) in backgound_layer_types] + whitelist_backgound_layer_types = [QgsRasterLayer, QgsVectorTileLayer, QgsTiledSceneLayer] + layers = [layer for layer in layers if type(layer) in whitelist_backgound_layer_types] else: layers = [] From 8f6065c46ec81b3d1b541f2aaf41c3a19eacb27a Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 7 Nov 2024 12:36:41 +0100 Subject: [PATCH 62/84] Rename layers and files tab --- Mergin/ui/ui_versions_viewer.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mergin/ui/ui_versions_viewer.ui b/Mergin/ui/ui_versions_viewer.ui index c44f4e0b..7e2b329a 100644 --- a/Mergin/ui/ui_versions_viewer.ui +++ b/Mergin/ui/ui_versions_viewer.ui @@ -321,7 +321,7 @@ - Layers + Updated layers @@ -331,7 +331,7 @@ - Files + Updated files From c9e21a85c7ec5d2aef1e09e7905b8375378a397e Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 7 Nov 2024 13:05:24 +0100 Subject: [PATCH 63/84] Add changeset summary in layer tab as well --- Mergin/version_viewer_dialog.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index c6e8d7c2..4c38c57f 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -520,8 +520,19 @@ def show_version_changes(self, version): self.diff_layers.append(vl) icon = icon_for_layer(vl) - self.layer_list.addItem(QListWidgetItem(icon, f"{vl.name()} ({vl.featureCount()})")) - # evenutally add a summary eg: \n 2 updated" + summary = self.find_changeset_summary_for_layer(vl.name(), self.version_details["changesets"]) + additional_info = [] + if summary["insert"]: + additional_info.append(f" Added : {summary['insert']}") + if summary["update"]: + additional_info.append(f", Updated : {summary['update']}") + if summary["delete"]: + additional_info.append(f", Deleted : {summary['delete']}") + + additional_summary = "\n" +",".join(additional_info) + + self.layer_list.addItem(QListWidgetItem(icon, vl.name() + additional_summary)) + if len(self.diff_layers) >= 1: self.toolbar.setEnabled(True) @@ -599,3 +610,11 @@ def zoom_selected(self): if self.current_diff: self.map_canvas.zoomToSelected([self.current_diff]) self.map_canvas.refresh() + + def find_changeset_summary_for_layer(self, layer_name:str, changesets: dict): + for gpkg_changes in changesets.values(): + for summary in gpkg_changes["summary"]: + if summary["table"] == layer_name: + return summary + + From 12d310d69d68f42f6a1640046eca0f1e10dd83ab Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 8 Nov 2024 14:21:11 +0100 Subject: [PATCH 64/84] Fix crash because of a edge case with layer named 'gpkh_something' --- Mergin/diff.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Mergin/diff.py b/Mergin/diff.py index b8a0db06..21b97a66 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -330,7 +330,7 @@ def make_version_changes_layers(project_path, version): layers = [] version_dir = os.path.join(project_path, ".mergin", ".cache", f"v{version}") - for f in glob.iglob("*.gpkg", root_dir=version_dir): + for f in glob.iglob(f"{version_dir}/*.gpkg"): gpkg_file = os.path.join(version_dir, f) schema_file = gpkg_file + "-schema.json" if not os.path.exists(schema_file): @@ -348,6 +348,10 @@ def make_version_changes_layers(project_path, version): diff = parse_diff(geodiff, changeset_file) for table_name in diff.keys(): + if table_name.startswith("gpkg_"): + # db schema reported by geodiff exclude layer named "gpkg_*" + # We skip it in the layer displayed to the user + continue fields, cols_to_fields = create_field_list(db_schema[table_name]) geom_type, geom_crs = get_layer_geometry_info(schema_json, table_name) @@ -377,7 +381,7 @@ def make_version_changes_layers(project_path, version): def find_changeset_file(file_name, version_dir): """Returns path to the diff file for the given version file""" - for f in glob.iglob("*.gpkg-diff*", root_dir=version_dir): + for f in glob.iglob(f"{version_dir}/*.gpkg-diff*"): if f.startswith(file_name): return os.path.join(version_dir, f) return None From 1468cdfb8d1c1f30f116475913c71385204f18ab Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 8 Nov 2024 14:44:01 +0100 Subject: [PATCH 65/84] Fix non spatial layer --- Mergin/version_viewer_dialog.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 4c38c57f..bf3f2edb 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -382,7 +382,7 @@ def closeEvent(self, event): def save_splitters_state(self): settings = QSettings() - settings.setValue("Mergin/VersionViewerSplitterSize", self.splitter.saveState()) + settings.setValue("Mergin/VersionViewerSplitterSize", self.splitter_map_table.saveState()) settings.setValue("Mergin/VersionViewerSplitterVericalSize", self.splitter_vertical.saveState()) def set_splitters_state(self): @@ -395,10 +395,10 @@ def set_splitters_state(self): state = settings.value("Mergin/VersionViewerSplitterSize") if state: - self.splitter.restoreState(state) + self.splitter_map_table.restoreState(state) else: height = max([self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()]) - self.splitter.setSizes([height, height]) + self.splitter_map_table.setSizes([height, height]) def fetch_from_server(self): @@ -492,6 +492,12 @@ def toggle_project_layers(self, checked): self.update_canvas_layers(layers) def update_canvas(self, layers): + + if self.current_diff.isSpatial() == False: + self.map_canvas.setEnabled(False) + else: + self.map_canvas.setEnabled(True) + self.update_canvas_layers(layers) self.update_canvas_extend(layers) From 572b91bb02691a4443883d16b5f9f3fb6c2074d6 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 8 Nov 2024 14:45:48 +0100 Subject: [PATCH 66/84] Fix last commit + format --- Mergin/diff.py | 4 ++-- Mergin/ui/ui_versions_viewer.ui | 2 +- Mergin/version_viewer_dialog.py | 26 ++++++++++++-------------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Mergin/diff.py b/Mergin/diff.py index 21b97a66..7f531c68 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -349,8 +349,8 @@ def make_version_changes_layers(project_path, version): for table_name in diff.keys(): if table_name.startswith("gpkg_"): - # db schema reported by geodiff exclude layer named "gpkg_*" - # We skip it in the layer displayed to the user + # db schema reported by geodiff exclude layer named "gpkg_*" + # We skip it in the layer displayed to the user continue fields, cols_to_fields = create_field_list(db_schema[table_name]) geom_type, geom_crs = get_layer_geometry_info(schema_json, table_name) diff --git a/Mergin/ui/ui_versions_viewer.ui b/Mergin/ui/ui_versions_viewer.ui index 7e2b329a..461965bd 100644 --- a/Mergin/ui/ui_versions_viewer.ui +++ b/Mergin/ui/ui_versions_viewer.ui @@ -208,7 +208,7 @@ - + Qt::Vertical diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index bf3f2edb..194558d5 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -35,11 +35,10 @@ QgsApplication, QgsFeatureRequest, QgsVectorLayerCache, - # Used to filter background map QgsRasterLayer, QgsTiledSceneLayer, - QgsVectorTileLayer + QgsVectorTileLayer, ) from qgis.gui import QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel @@ -300,7 +299,9 @@ def __init__(self, mc, parent=None): self.toggle_layers_button = QToolButton() self.toggle_layers_button.setDefaultAction(self.toggle_layers_action) self.toggle_layers_button.setText("Show background layers") - self.toggle_layers_button.setToolTip("Toggle the display of background layer(Raster and tiles) in the current project") + self.toggle_layers_button.setToolTip( + "Toggle the display of background layer(Raster and tiles) in the current project" + ) self.toggle_layers_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.toolbar.addWidget(self.toggle_layers_button) @@ -487,7 +488,7 @@ def toggle_project_layers(self, checked): self.toggle_layers_button.setText("Hide background layers") else: self.toggle_layers_button.setText("Show background layers") - + layers = self.collect_layers(checked) self.update_canvas_layers(layers) @@ -506,7 +507,7 @@ def update_canvas_layers(self, layers): self.map_canvas.refresh() def update_canvas_extend(self, layers): - self.map_canvas.setDestinationCrs( QgsProject.instance().crs()) + self.map_canvas.setDestinationCrs(QgsProject.instance().crs()) if layers: self.map_canvas.setDestinationCrs(layers[0].crs()) @@ -528,18 +529,17 @@ def show_version_changes(self, version): summary = self.find_changeset_summary_for_layer(vl.name(), self.version_details["changesets"]) additional_info = [] - if summary["insert"]: + if summary["insert"]: additional_info.append(f" Added : {summary['insert']}") - if summary["update"]: + if summary["update"]: additional_info.append(f", Updated : {summary['update']}") - if summary["delete"]: + if summary["delete"]: additional_info.append(f", Deleted : {summary['delete']}") - additional_summary = "\n" +",".join(additional_info) + additional_summary = "\n" + ",".join(additional_info) self.layer_list.addItem(QListWidgetItem(icon, vl.name() + additional_summary)) - if len(self.diff_layers) >= 1: self.toolbar.setEnabled(True) self.layer_list.setCurrentRow(0) @@ -559,7 +559,7 @@ def collect_layers(self, checked: bool): if checked: layers = iface.mapCanvas().layers() - #Filter only "Background" type + # Filter only "Background" type whitelist_backgound_layer_types = [QgsRasterLayer, QgsVectorTileLayer, QgsTiledSceneLayer] layers = [layer for layer in layers if type(layer) in whitelist_backgound_layer_types] else: @@ -617,10 +617,8 @@ def zoom_selected(self): self.map_canvas.zoomToSelected([self.current_diff]) self.map_canvas.refresh() - def find_changeset_summary_for_layer(self, layer_name:str, changesets: dict): + def find_changeset_summary_for_layer(self, layer_name: str, changesets: dict): for gpkg_changes in changesets.values(): for summary in gpkg_changes["summary"]: if summary["table"] == layer_name: return summary - - From 7e56254bfd14055ab698bf67ff5799e726dcdab0 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 12 Nov 2024 11:40:44 +0100 Subject: [PATCH 67/84] Fix formatting --- Mergin/version_viewer_dialog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 194558d5..019d4c5e 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -530,11 +530,11 @@ def show_version_changes(self, version): summary = self.find_changeset_summary_for_layer(vl.name(), self.version_details["changesets"]) additional_info = [] if summary["insert"]: - additional_info.append(f" Added : {summary['insert']}") + additional_info.append(f"{summary['insert']} added") if summary["update"]: - additional_info.append(f", Updated : {summary['update']}") + additional_info.append(f"{summary['update']} updated") if summary["delete"]: - additional_info.append(f", Deleted : {summary['delete']}") + additional_info.append(f"{summary['delete']} deleted") additional_summary = "\n" + ",".join(additional_info) @@ -571,7 +571,7 @@ def collect_layers(self, checked: bool): return layers def diff_layer_changed(self, index: int): - if index > len(self.diff_layers): + if index > len(self.diff_layers) or index < 0: return self.map_canvas.setLayers([]) From 7b3d20d0fcca4a2ad6209e6819d56326b3369ef8 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 12 Nov 2024 12:25:57 +0100 Subject: [PATCH 68/84] Fix crash mismatch between changeset and downloaded version --- Mergin/version_viewer_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 019d4c5e..3b21aecc 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -210,6 +210,7 @@ def run(self): if self.isInterruptionRequested(): self.quit() + return self.finished.emit("") From 3c4a0cc24e9072606ab4e2c5fee1ef34559289a8 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 12 Nov 2024 13:23:42 +0100 Subject: [PATCH 69/84] Fix non spatial layer again --- Mergin/version_viewer_dialog.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 3b21aecc..81f43f03 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -395,10 +395,15 @@ def set_splitters_state(self): else: self.splitter_vertical.setSizes([120, 200, 40]) + do_calc_height = True state = settings.value("Mergin/VersionViewerSplitterSize") if state: self.splitter_map_table.restoreState(state) - else: + + if self.splitter_map_table.sizes()[0] != 0: + do_calc_height = False + + if do_calc_height: height = max([self.map_canvas.minimumSizeHint().height(), self.attribute_table.minimumSizeHint().height()]) self.splitter_map_table.setSizes([height, height]) @@ -497,8 +502,12 @@ def update_canvas(self, layers): if self.current_diff.isSpatial() == False: self.map_canvas.setEnabled(False) + self.save_splitters_state() + self.splitter_map_table.setSizes([0, 1]) else: self.map_canvas.setEnabled(True) + self.set_splitters_state() + self.update_canvas_layers(layers) self.update_canvas_extend(layers) From ff732f685dad44384bf5eb791a91417c3d2141ab Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 13 Nov 2024 09:36:08 +0100 Subject: [PATCH 70/84] Fix projection again --- Mergin/version_viewer_dialog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 81f43f03..4d925d9d 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -520,12 +520,14 @@ def update_canvas_extend(self, layers): self.map_canvas.setDestinationCrs(QgsProject.instance().crs()) if layers: - self.map_canvas.setDestinationCrs(layers[0].crs()) extent = layers[0].extent() d = min(extent.width(), extent.height()) if d == 0: d = 1 extent = extent.buffered(d * 0.07) + + extent = self.map_canvas.mapSettings().layerExtentToOutputExtent( layers[0], extent ); + self.map_canvas.setExtent(extent) self.map_canvas.refresh() From cdb2d4346a54f93f67306e665c46cd57f7bea789 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Wed, 13 Nov 2024 09:37:09 +0100 Subject: [PATCH 71/84] Format --- Mergin/version_viewer_dialog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 4d925d9d..1a20ffdf 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -507,7 +507,6 @@ def update_canvas(self, layers): else: self.map_canvas.setEnabled(True) self.set_splitters_state() - self.update_canvas_layers(layers) self.update_canvas_extend(layers) @@ -526,7 +525,7 @@ def update_canvas_extend(self, layers): d = 1 extent = extent.buffered(d * 0.07) - extent = self.map_canvas.mapSettings().layerExtentToOutputExtent( layers[0], extent ); + extent = self.map_canvas.mapSettings().layerExtentToOutputExtent(layers[0], extent) self.map_canvas.setExtent(extent) self.map_canvas.refresh() From 53ad133785aa8c5006b5fcf5a0f3889a2156e610 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 19 Nov 2024 12:27:01 +0100 Subject: [PATCH 72/84] Fix zoom full --- Mergin/version_viewer_dialog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 1a20ffdf..ced43304 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -620,7 +620,12 @@ def add_all_to_project(self): def zoom_full(self): if self.current_diff: - self.map_canvas.setExtent(self.current_diff.extent()) + + layerExtent = self.current_diff.extent() + # transform extent + layerExtent = self.map_canvas.mapSettings().layerExtentToOutputExtent( self.current_diff, layerExtent ); + + self.map_canvas.setExtent(layerExtent) self.map_canvas.refresh() def zoom_selected(self): From fefbdd92a79bc02a3c58ba3078948dfc91e0b171 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 19 Nov 2024 12:35:12 +0100 Subject: [PATCH 73/84] Format --- Mergin/version_viewer_dialog.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index ced43304..6894ec69 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -620,11 +620,10 @@ def add_all_to_project(self): def zoom_full(self): if self.current_diff: - layerExtent = self.current_diff.extent() # transform extent - layerExtent = self.map_canvas.mapSettings().layerExtentToOutputExtent( self.current_diff, layerExtent ); - + layerExtent = self.map_canvas.mapSettings().layerExtentToOutputExtent(self.current_diff, layerExtent) + self.map_canvas.setExtent(layerExtent) self.map_canvas.refresh() From 7afbcfd8f28b763f9079d10357aa3883c13aaac6 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 29 Nov 2024 14:22:34 +0100 Subject: [PATCH 74/84] deal with empty geometry --- Mergin/diff.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mergin/diff.py b/Mergin/diff.py index 7f531c68..01e2a8a6 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -242,6 +242,9 @@ def diff_table_to_features(diff_table, schema_table, fields, cols_to_flds, db_co value = "?" if i == geom_col_index: + if value == None: + # Empty geometry + continue wkb_with_gpkg_hdr = base64.decodebytes(value.encode("ascii")) wkb = parse_gpkg_geom_encoding(wkb_with_gpkg_hdr) g = QgsGeometry() From 1950a216c24d4d299b5d5e529b4442c85db76c3a Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 2 Dec 2024 15:08:14 +0100 Subject: [PATCH 75/84] update UI --- Mergin/ui/ui_versions_viewer.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mergin/ui/ui_versions_viewer.ui b/Mergin/ui/ui_versions_viewer.ui index 461965bd..d04a8d65 100644 --- a/Mergin/ui/ui_versions_viewer.ui +++ b/Mergin/ui/ui_versions_viewer.ui @@ -321,7 +321,7 @@ - Updated layers + Changed layers @@ -331,7 +331,7 @@ - Updated files + Changed files From 4f6d13b96f32cd55d072462c47befe12a7a206b5 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Mon, 2 Dec 2024 15:08:35 +0100 Subject: [PATCH 76/84] changes after python-api-client --- Mergin/version_viewer_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 6894ec69..c3d2dcbb 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -242,7 +242,7 @@ def fetch_another_page(self): return versions = self.mc.project_versions_page( self.project_path, self.current_page, per_page=self.per_page, descending=True - ) + )[0] self.model.add_versions(versions) self.current_page += 1 From 44cd0f28256eb2cfe9da3a94d853aaa3b3fdd417 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 3 Dec 2024 12:57:18 +0100 Subject: [PATCH 77/84] rename function --- Mergin/version_viewer_dialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index c3d2dcbb..b1045093 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -240,10 +240,10 @@ def has_more_page(self): def fetch_another_page(self): if self.has_more_page() == False: return - versions = self.mc.project_versions_page( + page_versions, _ = self.mc.paginated_project_versions( self.project_path, self.current_page, per_page=self.per_page, descending=True - )[0] - self.model.add_versions(versions) + ) + self.model.add_versions(page_versions) self.current_page += 1 From 34c889861043be244bcd5f4280526a467f3cc985 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 6 Dec 2024 10:32:30 +0100 Subject: [PATCH 78/84] Fix diff fail when geometry is null --- Mergin/diff.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mergin/diff.py b/Mergin/diff.py index 01e2a8a6..8733da85 100644 --- a/Mergin/diff.py +++ b/Mergin/diff.py @@ -220,6 +220,8 @@ def diff_table_to_features(diff_table, schema_table, fields, cols_to_flds, db_co for i in range(len(db_row)): if i == geom_col_index: + if db_row[i] == None: + continue wkb = parse_gpkg_geom_encoding(db_row[i]) g = QgsGeometry() g.fromWkb(wkb) From 82ac5e5b2769bc910bfd3060b26510267ef04f60 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 10 Dec 2024 12:00:14 +0100 Subject: [PATCH 79/84] Change the arrow way to point back in time --- .../images/default/tabler_icons/history.svg | 56 +++++++++++++++++-- Mergin/images/white/tabler_icons/history.svg | 56 +++++++++++++++++-- 2 files changed, 100 insertions(+), 12 deletions(-) diff --git a/Mergin/images/default/tabler_icons/history.svg b/Mergin/images/default/tabler_icons/history.svg index 2721ea7f..e4ab8483 100644 --- a/Mergin/images/default/tabler_icons/history.svg +++ b/Mergin/images/default/tabler_icons/history.svg @@ -1,7 +1,51 @@ - - - - + + + + + + + - - diff --git a/Mergin/images/white/tabler_icons/history.svg b/Mergin/images/white/tabler_icons/history.svg index a72e8071..0b132ef2 100644 --- a/Mergin/images/white/tabler_icons/history.svg +++ b/Mergin/images/white/tabler_icons/history.svg @@ -1,7 +1,51 @@ - - - - + + + + + + + - - From 8944537edfd662521d02a70b45f83b6307581940 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Thu, 12 Dec 2024 12:50:51 +0100 Subject: [PATCH 80/84] Adress review comments : * Cleanup duplicate * Style --- Mergin/plugin.py | 2 +- Mergin/utils.py | 9 --------- Mergin/version_viewer_dialog.py | 8 ++++---- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/Mergin/plugin.py b/Mergin/plugin.py index d20cd60a..3c526b9c 100644 --- a/Mergin/plugin.py +++ b/Mergin/plugin.py @@ -158,7 +158,7 @@ def initGui(self): add_to_menu=True, add_to_toolbar=None, ) - self.history_dock_action = self.add_action( + self.add_action( "history.svg", text="Project History", callback=self.open_project_history_window, diff --git a/Mergin/utils.py b/Mergin/utils.py index 4299e55c..03943bfb 100644 --- a/Mergin/utils.py +++ b/Mergin/utils.py @@ -1509,15 +1509,6 @@ def get_layer_by_path(path): return layer -def check_mergin_subdirs(directory): - """Check if the directory has a Mergin Maps project subdir (.mergin).""" - for root, dirs, files in os.walk(directory): - for name in dirs: - if name == ".mergin": - return os.path.join(root, name) - return False - - def contextual_date(date_string): """Converts datetime string returned by the server into contextual duration string, e.g. 'N hours/days/month ago' diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index b1045093..a3efc443 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -384,19 +384,19 @@ def closeEvent(self, event): def save_splitters_state(self): settings = QSettings() - settings.setValue("Mergin/VersionViewerSplitterSize", self.splitter_map_table.saveState()) - settings.setValue("Mergin/VersionViewerSplitterVericalSize", self.splitter_vertical.saveState()) + settings.setValue("Mergin/versionViewerSplitterSize", self.splitter_map_table.saveState()) + settings.setValue("Mergin/versionViewerSplitterVericalSize", self.splitter_vertical.saveState()) def set_splitters_state(self): settings = QSettings() - state_vertical = settings.value("Mergin/VersionViewerSplitterVericalSize") + state_vertical = settings.value("Mergin/versionViewerSplitterVericalSize") if state_vertical: self.splitter_vertical.restoreState(state_vertical) else: self.splitter_vertical.setSizes([120, 200, 40]) do_calc_height = True - state = settings.value("Mergin/VersionViewerSplitterSize") + state = settings.value("Mergin/versionViewerSplitterSize") if state: self.splitter_map_table.restoreState(state) From 646e3d668b70734b96a051cc68e054a8a7bfec6b Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 13 Dec 2024 12:45:07 +0100 Subject: [PATCH 81/84] Apply suggestions from review: * auto restore window geometry * handle disconnection from internet --- Mergin/version_viewer_dialog.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index a3efc443..c7aa6cb1 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -40,7 +40,7 @@ QgsTiledSceneLayer, QgsVectorTileLayer, ) -from qgis.gui import QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel +from qgis.gui import QgsGui, QgsMapToolPan, QgsAttributeTableModel, QgsAttributeTableFilterModel from .utils import ( @@ -262,8 +262,12 @@ def __init__(self, mc, parent=None): QDialog.__init__(self, parent) self.ui = uic.loadUi(ui_file, self) + QgsGui.instance().enableAutoGeometryRestore(self) + self.mc = mc + self.failed_to_fetch = False + self.project_path = mergin_project_local_path() self.mp = MerginProject(self.project_path) @@ -278,10 +282,14 @@ def __init__(self, mc, parent=None): self.has_selected_latest = False - self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.versionModel) - self.diff_downloader = None + try: + self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.versionModel) + self.diff_downloader = None - self.fetcher.fetch_another_page() + self.fetcher.fetch_another_page() + except ClientError as e: + self.failed_to_fetch = True + return height = 30 self.toolbar.setMinimumHeight(height) @@ -358,7 +366,10 @@ def __init__(self, mc, parent=None): self.versionModel.current_version = self.mp.version() def exec(self): - + if self.failed_to_fetch: + msg = f"Client error : Failed to reach history version for project {self.project_path}" + QMessageBox.critical(None, "Failed requesting history", msg, QMessageBox.Close) + return try: ws_id = self.mp.workspace_id() except ClientError as e: From dd9ca4f2d8f11986e2748e111a0a4f44a9e180b7 Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Fri, 13 Dec 2024 12:48:29 +0100 Subject: [PATCH 82/84] Add waiting cursor in version viewer open --- Mergin/version_viewer_dialog.py | 186 ++++++++++++++++---------------- 1 file changed, 96 insertions(+), 90 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index c7aa6cb1..066533b6 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -28,7 +28,8 @@ QItemSelectionModel, ) -from qgis.utils import iface +from qgis.utils import iface, OverrideCursor + from qgis.core import ( QgsProject, QgsMessageLog, @@ -262,108 +263,113 @@ def __init__(self, mc, parent=None): QDialog.__init__(self, parent) self.ui = uic.loadUi(ui_file, self) - QgsGui.instance().enableAutoGeometryRestore(self) - - self.mc = mc - - self.failed_to_fetch = False + with OverrideCursor(Qt.WaitCursor): + QgsGui.instance().enableAutoGeometryRestore(self) - self.project_path = mergin_project_local_path() - self.mp = MerginProject(self.project_path) + self.mc = mc - self.set_splitters_state() + self.failed_to_fetch = False - self.versionModel = VersionsTableModel() - self.history_treeview.setModel(self.versionModel) - self.history_treeview.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) + self.project_path = mergin_project_local_path() + self.mp = MerginProject(self.project_path) - self.selectionModel: QItemSelectionModel = self.history_treeview.selectionModel() - self.selectionModel.currentChanged.connect(self.current_version_changed) - - self.has_selected_latest = False + self.set_splitters_state() - try: - self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.versionModel) - self.diff_downloader = None + self.versionModel = VersionsTableModel() + self.history_treeview.setModel(self.versionModel) + self.history_treeview.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) - self.fetcher.fetch_another_page() - except ClientError as e: - self.failed_to_fetch = True - return + self.selectionModel: QItemSelectionModel = self.history_treeview.selectionModel() + self.selectionModel.currentChanged.connect(self.current_version_changed) - height = 30 - self.toolbar.setMinimumHeight(height) + self.has_selected_latest = False - self.history_control.setMinimumHeight(height) - self.history_control.setVisible(False) + try: + self.fetcher = VersionsFetcher(self.mc, self.mp.project_full_name(), self.versionModel) + self.diff_downloader = None - self.toggle_layers_action = QAction( - QgsApplication.getThemeIcon("/mActionAddLayer.svg"), "Hide background layers", self - ) - self.toggle_layers_action.setCheckable(True) - self.toggle_layers_action.setChecked(True) - self.toggle_layers_action.toggled.connect(self.toggle_project_layers) - - # We use a ToolButton instead of simple action to dislay both icon AND text - self.toggle_layers_button = QToolButton() - self.toggle_layers_button.setDefaultAction(self.toggle_layers_action) - self.toggle_layers_button.setText("Show background layers") - self.toggle_layers_button.setToolTip( - "Toggle the display of background layer(Raster and tiles) in the current project" - ) - self.toggle_layers_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) - self.toolbar.addWidget(self.toggle_layers_button) + self.fetcher.fetch_another_page() + except ClientError as e: + self.failed_to_fetch = True + return - self.toolbar.addSeparator() + height = 30 + self.toolbar.setMinimumHeight(height) - self.zoom_full_action = QAction(QgsApplication.getThemeIcon("/mActionZoomFullExtent.svg"), "Zoom Full", self) - self.zoom_full_action.triggered.connect(self.zoom_full) + self.history_control.setMinimumHeight(height) + self.history_control.setVisible(False) - self.toolbar.addAction(self.zoom_full_action) + self.toggle_layers_action = QAction( + QgsApplication.getThemeIcon("/mActionAddLayer.svg"), "Hide background layers", self + ) + self.toggle_layers_action.setCheckable(True) + self.toggle_layers_action.setChecked(True) + self.toggle_layers_action.toggled.connect(self.toggle_project_layers) - self.zoom_selected_action = QAction( - QgsApplication.getThemeIcon("/mActionZoomToSelected.svg"), "Zoom To Selection", self - ) - self.zoom_selected_action.triggered.connect(self.zoom_selected) - - self.toolbar.addAction(self.zoom_selected_action) - - btn_add_changes = QPushButton("Add to project") - btn_add_changes.setIcon(QgsApplication.getThemeIcon("/mActionAdd.svg")) - menu = QMenu() - add_current_action = menu.addAction(QIcon(icon_path("file-plus.svg")), "Add current changes layer to project") - add_current_action.triggered.connect(self.add_current_to_project) - add_all_action = menu.addAction(QIcon(icon_path("folder-plus.svg")), "Add all changes layers to project") - add_all_action.triggered.connect(self.add_all_to_project) - btn_add_changes.setMenu(menu) - - self.toolbar.addWidget(btn_add_changes) - self.toolbar.setIconSize(iface.iconSize()) - - self.map_canvas.enableAntiAliasing(True) - self.map_canvas.setSelectionColor(QColor(Qt.cyan)) - self.pan_tool = QgsMapToolPan(self.map_canvas) - self.map_canvas.setMapTool(self.pan_tool) - - self.current_diff = None - self.diff_layers = [] - self.filter_model = None - self.layer_list.currentRowChanged.connect(self.diff_layer_changed) - - self.icons = { - "added": "plus.svg", - "removed": "trash.svg", - "updated": "pencil.svg", - "renamed": "pencil.svg", - "table": "table.svg", - } - self.model_detail = QStandardItemModel() - self.model_detail.setHorizontalHeaderLabels(["Details"]) - - self.details_treeview.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.details_treeview.setModel(self.model_detail) - - self.versionModel.current_version = self.mp.version() + # We use a ToolButton instead of simple action to dislay both icon AND text + self.toggle_layers_button = QToolButton() + self.toggle_layers_button.setDefaultAction(self.toggle_layers_action) + self.toggle_layers_button.setText("Show background layers") + self.toggle_layers_button.setToolTip( + "Toggle the display of background layer(Raster and tiles) in the current project" + ) + self.toggle_layers_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.toolbar.addWidget(self.toggle_layers_button) + + self.toolbar.addSeparator() + + self.zoom_full_action = QAction( + QgsApplication.getThemeIcon("/mActionZoomFullExtent.svg"), "Zoom Full", self + ) + self.zoom_full_action.triggered.connect(self.zoom_full) + + self.toolbar.addAction(self.zoom_full_action) + + self.zoom_selected_action = QAction( + QgsApplication.getThemeIcon("/mActionZoomToSelected.svg"), "Zoom To Selection", self + ) + self.zoom_selected_action.triggered.connect(self.zoom_selected) + + self.toolbar.addAction(self.zoom_selected_action) + + btn_add_changes = QPushButton("Add to project") + btn_add_changes.setIcon(QgsApplication.getThemeIcon("/mActionAdd.svg")) + menu = QMenu() + add_current_action = menu.addAction( + QIcon(icon_path("file-plus.svg")), "Add current changes layer to project" + ) + add_current_action.triggered.connect(self.add_current_to_project) + add_all_action = menu.addAction(QIcon(icon_path("folder-plus.svg")), "Add all changes layers to project") + add_all_action.triggered.connect(self.add_all_to_project) + btn_add_changes.setMenu(menu) + + self.toolbar.addWidget(btn_add_changes) + self.toolbar.setIconSize(iface.iconSize()) + + self.map_canvas.enableAntiAliasing(True) + self.map_canvas.setSelectionColor(QColor(Qt.cyan)) + self.pan_tool = QgsMapToolPan(self.map_canvas) + self.map_canvas.setMapTool(self.pan_tool) + + self.current_diff = None + self.diff_layers = [] + self.filter_model = None + self.layer_list.currentRowChanged.connect(self.diff_layer_changed) + + self.icons = { + "added": "plus.svg", + "removed": "trash.svg", + "updated": "pencil.svg", + "renamed": "pencil.svg", + "table": "table.svg", + } + self.model_detail = QStandardItemModel() + self.model_detail.setHorizontalHeaderLabels(["Details"]) + + self.details_treeview.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.details_treeview.setModel(self.model_detail) + + self.versionModel.current_version = self.mp.version() def exec(self): if self.failed_to_fetch: From 5e9b020eca2d18c41776d1c56e35f83c96a3e5fc Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 7 Jan 2025 13:43:39 +0100 Subject: [PATCH 83/84] Adress review comments --- Mergin/version_viewer_dialog.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 066533b6..39aa567a 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -75,7 +75,7 @@ def __init__(self, parent=None): super().__init__(parent) # Keep ordered - self.versions = deque() + self.versions = []] self.oldest = None self.latest = None @@ -85,12 +85,12 @@ def __init__(self, parent=None): self.current_version = None def latest_version(self): - if len(self.versions) == 0: + if not self.versions: return None return int_version(self.versions[0]["name"]) def oldest_version(self): - if len(self.versions) == 0: + if not self.versions: return None return int_version(self.versions[-1]["name"]) @@ -125,8 +125,9 @@ def data(self, index, role=Qt.DisplayRole): font.setBold(True) return font elif role == Qt.ToolTipRole: - if index.column() == 2: - return format_datetime(self.versions[idx]["created"]) + return f"""Version: {self.versions[idx]['name'] } +Author: {self.versions[idx]['author']} +Date: {format_datetime(self.versions[idx]['created'])}""" elif role == VersionsTableModel.VERSION: return int_version(self.versions[idx]["name"]) elif role == VersionsTableModel.VERSION_NAME: @@ -280,7 +281,7 @@ def __init__(self, mc, parent=None): self.history_treeview.verticalScrollBar().valueChanged.connect(self.on_scrollbar_changed) self.selectionModel: QItemSelectionModel = self.history_treeview.selectionModel() - self.selectionModel.currentChanged.connect(self.current_version_changed) + self.selectionModel.currentChanged.connect(self.selected_version_changed) self.has_selected_latest = False @@ -437,7 +438,7 @@ def on_scrollbar_changed(self, value): if self.ui.history_treeview.verticalScrollBar().maximum() <= value: self.fetch_from_server() - def current_version_changed(self, current_index, previous_index): + def selected_version_changed(self, current_index, previous_index): # Update the ui when the selected version change item = self.versionModel.item_from_index(current_index) version_name = item["name"] From 6bc441c205ea7ab64a886e79daa334c3fe4d29ac Mon Sep 17 00:00:00 2001 From: Valentin Buira Date: Tue, 7 Jan 2025 13:58:20 +0100 Subject: [PATCH 84/84] typo --- Mergin/version_viewer_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mergin/version_viewer_dialog.py b/Mergin/version_viewer_dialog.py index 39aa567a..e424a609 100644 --- a/Mergin/version_viewer_dialog.py +++ b/Mergin/version_viewer_dialog.py @@ -75,7 +75,7 @@ def __init__(self, parent=None): super().__init__(parent) # Keep ordered - self.versions = []] + self.versions = [] self.oldest = None self.latest = None