Source code for riverarchitect.gui.qt.main

"""Qt main window: a tabbed shell with one tab per module.

Structurally the same as the tkinter window it replaces, with the differences a real
toolkit makes possible: the menu bar belongs to the window rather than being rebuilt on
every tab change, the status bar reports the project directory, and a tab that fails to
construct is replaced by a message instead of taking the window down.
"""

import glob
import logging
import os

from ... import __version__, config, guide
from .qtcompat import (
    QAction, QApplication, QDesktopServices, QFileDialog, QIcon, QLabel,
    QMainWindow, QMessageBox, QTabWidget, QUrl, QVBoxLayout, QWidget,
    Qt, QT_BINDING, exec_app,
)
from .getstarted_tab import GetStartedTab
from .lifespan_tab import LifespanTab
from .mapping_tab import MappingTab
from .projectmaker_tab import ProjectMakerTab
from .maxlifespan_tab import MaxLifespanTab
from .recruitment_tab import RecruitmentTab
from .riverbuilder_tab import RiverBuilderTab
from .sharc_tab import SharcTab
from .stranding_tab import StrandingTab
from .terraforming_tab import TerraformingTab
from .volume_tab import VolumeTab

__all__ = ["RiverArchitectWindow", "run", "TAB_GROUPS", "TABS"]

#: Top-level tabs and their sub-tabs, reproducing the grouping of the ArcGIS version:
#: a module either stands alone or sits inside a themed group. ``None`` means the entry is
#: a single tab rather than a group.
TAB_GROUPS = (
    ("Get Started", (GetStartedTab,)),
    ("Lifespan", (LifespanTab, MaxLifespanTab)),
    ("Morphology", (TerraformingTab, RiverBuilderTab, VolumeTab)),
    ("Ecohydraulics", (SharcTab, StrandingTab, RecruitmentTab)),
    ("Project Maker", (ProjectMakerTab,)),
    ("Maps", (MappingTab,)),
)

#: Flat list of every module tab, in the order they appear.
TABS = tuple(factory for _group, factories in TAB_GROUPS for factory in factories)


[docs] class RiverArchitectWindow(QMainWindow): """Top-level window holding one tab per module.""" def __init__(self): super().__init__() self.logger = logging.getLogger("riverarchitect") self.setWindowTitle("River Architect %s" % __version__) self.resize(880, 620) self.tabs = QTabWidget() self.tabs.setDocumentMode(True) self.setCentralWidget(self.tabs) self.module_tabs = [] self.groups = {} for group, factories in TAB_GROUPS: if len(factories) == 1: # A group of one is just a tab; nesting it would only add a click. self.tabs.addTab(self._make_tab(factories[0]), group) continue inner = QTabWidget() inner.setDocumentMode(True) for factory in factories: inner.addTab(self._make_tab(factory), factory.title) self.groups[group] = inner self.tabs.addTab(inner, group) self._build_menus() self.project_label = QLabel() self.statusBar().addPermanentWidget(self.project_label) self._update_status() def _make_tab(self, factory): """Build a module tab, or a message standing in for one that could not load.""" try: tab = factory() except Exception as exc: # a broken module must not take the window down self.logger.error("Could not load the %s tab: %s", factory.title, exc) return self._placeholder(factory.title, exc) self.module_tabs.append(tab) return tab def _placeholder(self, label, exc): widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins(24, 24, 24, 24) heading = QLabel("The %s tab could not be loaded." % label) font = heading.font() font.setBold(True) heading.setFont(font) layout.addWidget(heading) detail = QLabel(str(exc)) detail.setWordWrap(True) detail.setStyleSheet("color: palette(mid);") layout.addWidget(detail) layout.addStretch(1) return widget # ------------------------------------------------------------------- menus def _build_menus(self): project_menu = self.menuBar().addMenu("&Project") action = QAction("Set project directory ...", self) action.triggered.connect(self.choose_project_home) project_menu.addAction(action) project_menu.addSeparator() action = QAction("&Quit", self) action.setShortcut("Ctrl+Q") action.triggered.connect(self.close) project_menu.addAction(action) units_menu = self.menuBar().addMenu("&Units") self.unit_actions = {} for unit, label in (("us", "U.S. customary"), ("si", "SI (metric)")): action = QAction(label, self, checkable=True) action.setChecked(unit == "us") action.triggered.connect(lambda _checked=False, u=unit: self.set_unit(u)) units_menu.addAction(action) self.unit_actions[unit] = action tools_menu = self.menuBar().addMenu("&Tools") action = QAction("Reconcile NoData in a condition ...", self) action.triggered.connect(self.run_reconcile_nodata) tools_menu.addAction(action) help_menu = self.menuBar().addMenu("&Help") action = QAction("Documentation", self) action.setShortcut("F1") action.triggered.connect(self.open_documentation) help_menu.addAction(action) action = QAction(guide.TITLE, self) action.triggered.connect(self.open_live_guide) help_menu.addAction(action) help_menu.addSeparator() action = QAction("About", self) action.triggered.connect(self.show_about) help_menu.addAction(action) # -------------------------------------------------------------- navigation
[docs] def select_tab(self, group, tab=None): """Bring a module tab to the front. Used by the Live Guide. Args: group (str): the top-level tab, as :data:`TAB_GROUPS` names it. tab (str): the module tab within it. Defaults to the group itself. Returns: bool: True when the tab was found and selected. """ tab = tab or group for index in range(self.tabs.count()): if self.tabs.tabText(index) != group: continue self.tabs.setCurrentIndex(index) inner = self.groups.get(group) if inner is None: return True for inner_index in range(inner.count()): if inner.tabText(inner_index) == tab: inner.setCurrentIndex(inner_index) return True return tab == group return False
# ----------------------------------------------------------------- actions
[docs] @staticmethod def open_documentation(): QDesktopServices.openUrl(QUrl(guide.DOCS_URL))
[docs] def open_live_guide(self): """Open the step-by-step walkthrough of the sample-data example.""" from .guide_window import open_guide open_guide(self)
[docs] def set_unit(self, unit): """Switch the unit system for every tab.""" for key, action in self.unit_actions.items(): action.setChecked(key == unit) for tab in self.module_tabs: tab.set_unit(unit)
[docs] def choose_project_home(self): directory = QFileDialog.getExistingDirectory( self, "Select the River Architect project directory", config.project_home()) if not directory: return config.set_project_home(directory) for tab in self.module_tabs: tab.on_project_home_change() self._update_status() conditions = self.module_tabs[0].condition_list if self.module_tabs else [] QMessageBox.information( self, "Project directory", "Project directory set to:\n%s\n\n%d condition(s) found." % (directory, len(conditions)))
[docs] def run_reconcile_nodata(self): from ...tools import reconcile_nodata directory = QFileDialog.getExistingDirectory( self, "Select a condition folder to reconcile", config.dir_conditions()) if not directory: return rasters = sorted(glob.glob(os.path.join(directory, "*.tif"))) if not rasters: QMessageBox.warning(self, "Reconcile NoData", "No GeoTIFFs found in\n%s" % directory) return confirm = QMessageBox.question( self, "Reconcile NoData", "Rewrite %d raster(s) in\n%s\nto NoData = %s?\n\n" "The NoData mask is preserved exactly; only the sentinel changes." % (len(rasters), directory, config.NODATA)) if confirm != QMessageBox.StandardButton.Yes: return changed = failed = 0 for path in rasters: try: if reconcile_nodata.reconcile(path): changed += 1 except Exception as exc: failed += 1 self.logger.error("Could not reconcile %s: %s", path, exc) message = "%d raster(s) updated." % changed if failed: message += "\n%d could not be read; see the log." % failed QMessageBox.information(self, "Reconcile NoData", message)
[docs] def show_about(self): QMessageBox.about( self, "About River Architect", "<b>River Architect %s</b><br><br>" "Open-source analysis and design of fluvial ecosystems.<br>" "<a href='https://riverarchitect.readthedocs.io/'>" "riverarchitect.readthedocs.io</a><br><br>" "Qt binding: %s<br>" "Project directory:<br>%s" % (__version__, QT_BINDING, config.project_home()))
def _update_status(self): self.project_label.setText("Project: %s" % config.project_home())
[docs] def run(argv=None): """Create the application, show the window and run the event loop.""" app = QApplication.instance() or QApplication(argv or []) app.setApplicationName("River Architect") app.setApplicationDisplayName("River Architect") app.setApplicationVersion(__version__) app.setDesktopFileName(config.APP_ID) icon = QIcon(config.icon_path()) app.setWindowIcon(icon) window = RiverArchitectWindow() window.setWindowIcon(icon) window.show() return exec_app(app)