diff --git a/LICENSE b/LICENSE
index 1f342036..4b972174 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2022 Jordan Cook
+Copyright (c) 2024 Jordan Cook
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/docs/conf.py b/docs/conf.py
index f9a644d4..b53e0782 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -7,7 +7,7 @@
# Basic config
project = 'Naturtag'
-copyright = '2022, Jordan Cook'
+copyright = '2024, Jordan Cook'
author = 'Jordan Cook'
html_static_path = ['_static']
templates_path = ['_templates']
diff --git a/naturtag/app/app.py b/naturtag/app/app.py
index 61408d92..25fd8154 100755
--- a/naturtag/app/app.py
+++ b/naturtag/app/app.py
@@ -86,28 +86,6 @@ def __init__(self, app: NaturtagApp):
self.taxon_controller.on_message.connect(self.info)
self.observation_controller.on_message.connect(self.info)
- # Select observation/taxon from image context menu, ID input fields, and iconic taxa filters
- self.image_controller.gallery.on_select_taxon.connect(self.taxon_controller.select_taxon)
- self.image_controller.gallery.on_select_observation.connect(
- self.observation_controller.select_observation
- )
- self.image_controller.on_select_observation_id.connect(
- self.observation_controller.select_observation
- )
- self.image_controller.on_select_taxon_id.connect(self.taxon_controller.select_taxon)
- self.taxon_controller.search.iconic_taxon_filters.on_select.connect(
- self.taxon_controller.select_taxon
- )
-
- # Update photo tab when a taxon is selected
- self.taxon_controller.on_select.connect(self.image_controller.select_taxon)
-
- # Update photo and taxon tabs when an observation is selected
- self.observation_controller.on_select.connect(self.image_controller.select_observation)
- self.observation_controller.on_select.connect(
- lambda obs: self.taxon_controller.display_taxon(obs.taxon, notify=False)
- )
-
# Settings that take effect immediately
self.settings_menu.all_ranks.on_click.connect(self.taxon_controller.search.reset_ranks)
self.settings_menu.dark_mode.on_click.connect(set_theme)
@@ -133,12 +111,48 @@ def __init__(self, app: NaturtagApp):
)
self.tabs.setTabVisible(self.log_tab_idx, self.app.settings.show_logs)
- # Switch to different tab if requested from Photos tab
- self.image_controller.on_select_taxon_tab.connect(
- lambda: self.tabs.setCurrentWidget(self.taxon_controller)
+ # Photos tab: view taxon and switch tab
+ self.image_controller.gallery.on_view_taxon_id.connect(
+ self.taxon_controller.display_taxon_by_id
+ )
+ self.image_controller.gallery.on_view_taxon_id.connect(self.switch_tab_taxa)
+ self.image_controller.on_view_taxon_id.connect(self.taxon_controller.display_taxon_by_id)
+ self.image_controller.on_view_taxon_id.connect(self.switch_tab_taxa)
+
+ # Photos tab: view observation and switch tab
+ self.image_controller.gallery.on_view_observation_id.connect(
+ self.observation_controller.display_observation_by_id
)
- self.image_controller.on_select_observation_tab.connect(
- lambda: self.tabs.setCurrentWidget(self.observation_controller)
+ self.image_controller.gallery.on_view_observation_id.connect(self.switch_tab_observations)
+ self.image_controller.on_view_observation_id.connect(
+ self.observation_controller.display_observation_by_id
+ )
+ self.image_controller.on_view_observation_id.connect(self.switch_tab_observations)
+
+ # Species tab: View observation and switch tab
+ # self.taxon_controller.taxon_info.on_view_observations.connect(
+ # lambda obs: self.observation_controller.display_observation(obs, notify=False)
+ # )
+ # self.taxon_controller.taxon_info.on_view_observations.connect(self.switch_tab_observations)
+
+ # Species tab: Select taxon for tagging and switch to Photos tab
+ self.taxon_controller.taxon_info.on_select.connect(self.image_controller.select_taxon)
+ self.taxon_controller.taxon_info.on_select.connect(
+ lambda: self.tabs.setCurrentWidget(self.image_controller)
+ )
+
+ # Observations tab: View taxon and switch tab
+ self.observation_controller.obs_info.on_view_taxon.connect(
+ lambda taxon: self.taxon_controller.display_taxon(taxon, notify=False)
+ )
+ self.observation_controller.obs_info.on_view_taxon.connect(self.switch_tab_taxa)
+
+ # Observations tab: Select observation for tagging and switch to Photos tab
+ self.observation_controller.obs_info.on_select.connect(
+ self.image_controller.select_observation
+ )
+ self.observation_controller.obs_info.on_select.connect(
+ lambda: self.tabs.setCurrentWidget(self.image_controller)
)
# Connect file picker <--> recent/favorite dirs
@@ -176,7 +190,7 @@ def __init__(self, app: NaturtagApp):
QShortcut(QKeySequence('F9'), self).activated.connect(self.reload_qss)
demo_images = list((ASSETS_DIR / 'demo_images').glob('*.jpg'))
self.image_controller.gallery.load_images(demo_images) # type: ignore
- self.observation_controller.select_observation(56830941)
+ self.observation_controller.display_observation_by_id(56830941)
def check_username(self):
"""If username isn't saved, show popup dialog to prompt user to enter it"""
@@ -255,6 +269,15 @@ def show_settings(self):
"""Show the settings menu"""
self.settings_menu.show()
+ def switch_tab_observations(self):
+ self.tabs.setCurrentWidget(self.observation_controller)
+
+ def switch_tab_taxa(self):
+ self.tabs.setCurrentWidget(self.taxon_controller)
+
+ def switch_tab_photos(self):
+ self.tabs.setCurrentWidget(self.image_controller)
+
def toggle_fullscreen(self) -> bool:
"""Toggle fullscreen, and change icon for toolbar fullscreen button"""
if not self.isFullScreen():
diff --git a/naturtag/controllers/image_controller.py b/naturtag/controllers/image_controller.py
index 474807a8..0694201c 100644
--- a/naturtag/controllers/image_controller.py
+++ b/naturtag/controllers/image_controller.py
@@ -2,10 +2,10 @@
from typing import Optional
from pyinaturalist import Observation, Taxon
-from PySide6.QtCore import Qt, Signal, Slot
+from PySide6.QtCore import Qt, QThread, Signal, Slot
from PySide6.QtWidgets import QApplication, QGroupBox, QLabel, QSizePolicy
-from naturtag.controllers import BaseController, ImageGallery
+from naturtag.controllers import BaseController, ImageGallery, get_app
from naturtag.metadata import MetaMetadata, _refresh_tags, tag_images
from naturtag.utils import get_ids_from_url
from naturtag.widgets import (
@@ -24,10 +24,8 @@ class ImageController(BaseController):
"""Controller for selecting and tagging local image files"""
on_new_metadata = Signal(MetaMetadata) #: Metadata for an image was updated
- on_select_taxon_id = Signal(int) #: A taxon ID was entered
- on_select_taxon_tab = Signal() #: Request to switch to taxon tab
- on_select_observation_id = Signal(int) #: An observation ID was entered
- on_select_observation_tab = Signal() #: Request to switch to observation tab
+ on_view_taxon_id = Signal(int) #: Request to switch to taxon tab
+ on_view_observation_id = Signal(int) #: Request to switch to observation tab
def __init__(self):
super().__init__()
@@ -45,16 +43,14 @@ def __init__(self):
# Input fields
inputs_layout = VerticalLayout(group_box)
- self.input_obs_id = IdInput()
- inputs_layout.addWidget(QLabel('Observation ID:'))
- inputs_layout.addWidget(self.input_obs_id)
self.input_taxon_id = IdInput()
+ self.input_taxon_id.on_select.connect(self.select_taxon_by_id)
inputs_layout.addWidget(QLabel('Taxon ID:'))
inputs_layout.addWidget(self.input_taxon_id)
-
- # Notify other controllers when an ID is selected from input text
- self.input_obs_id.on_select.connect(self.on_select_observation_id)
- self.input_taxon_id.on_select.connect(self.on_select_taxon_id)
+ self.input_obs_id = IdInput()
+ self.input_obs_id.on_select.connect(self.select_observation_by_id)
+ inputs_layout.addWidget(QLabel('Observation ID:'))
+ inputs_layout.addWidget(self.input_obs_id)
# Selected taxon/observation info
group_box = QGroupBox('Metadata source')
@@ -63,15 +59,11 @@ def __init__(self):
group_box.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
top_section_layout.addWidget(group_box)
self.data_source_card = HorizontalLayout(group_box)
-
- # Clear info when clearing an input field
- self.input_obs_id.on_clear.connect(self.data_source_card.clear)
- self.input_taxon_id.on_clear.connect(self.data_source_card.clear)
+ self.selected_taxon_id: Optional[int] = None
+ self.selected_observation_id: Optional[int] = None
# Image gallery
self.gallery = ImageGallery()
- self.gallery.on_select_observation.connect(self.on_select_observation_tab)
- self.gallery.on_select_taxon.connect(self.on_select_taxon_tab)
photo_layout.addWidget(self.gallery)
def run(self):
@@ -81,7 +73,7 @@ def run(self):
self.info('Select images to tag')
return
- obs_id, taxon_id = self.input_obs_id.text(), self.input_taxon_id.text()
+ obs_id, taxon_id = self.selected_taxon_id, self.selected_observation_id
if not (obs_id or taxon_id):
self.info('Select either an observation or an organism to tag images with')
return
@@ -139,36 +131,71 @@ def paste(self):
# Check for IDs if an iNat URL was pasted
observation_id, taxon_id = get_ids_from_url(text)
if observation_id:
- self.on_select_observation_id.emit(observation_id)
+ self.select_observation_by_id(observation_id)
elif taxon_id:
- self.on_select_taxon_id.emit(taxon_id)
+ self.select_taxon_by_id(taxon_id)
# If not an iNat URL, check for valid image paths
else:
self.gallery.load_images(text.splitlines())
+ # Note: These methods duplicate "display_x_by_id" controller methods, but attempts at code reuse
+ # added too much spaghetti
+ def select_taxon_by_id(self, taxon_id: int):
+ """Load a taxon by ID (pasted or directly entered)"""
+ if self.selected_taxon_id == taxon_id:
+ return
+
+ app = get_app()
+ logger.info(f'Loading taxon {taxon_id}')
+ future = app.threadpool.schedule(
+ lambda: app.client.taxa(taxon_id, locale=app.settings.locale),
+ priority=QThread.HighPriority,
+ )
+ future.on_result.connect(self.select_taxon)
+
+ def select_observation_by_id(self, observation_id: int):
+ """Load an observation by ID (pasted or directly entered)"""
+ if self.selected_observation_id == observation_id:
+ return
+
+ app = get_app()
+ logger.info(f'Loading observation {observation_id}')
+ future = app.threadpool.schedule(
+ lambda: app.client.observations(observation_id, taxonomy=True),
+ priority=QThread.HighPriority,
+ )
+ future.on_result.connect(self.select_observation)
+
@Slot(Taxon)
def select_taxon(self, taxon: Taxon):
- """Update input info from a taxon object (loaded from Species tab)"""
- if self.input_taxon_id.text() == str(taxon.id):
+ """Update metadata info from a taxon object"""
+ if self.selected_taxon_id == taxon.id:
return
- self.input_taxon_id.set_id(taxon.id)
+ self.selected_taxon_id = taxon.id
+ self.selected_observation_id = None
+ self.input_obs_id.clear()
+ self.input_taxon_id.clear()
self.data_source_card.clear()
+
card = TaxonInfoCard(taxon=taxon, delayed_load=False)
- card.on_click.connect(self.on_select_taxon_tab)
+ card.on_click.connect(self.on_view_taxon_id)
self.data_source_card.addWidget(card)
@Slot(Observation)
def select_observation(self, observation: Observation):
- """Update input info from an observation object (loaded from Observations tab)"""
- if self.input_obs_id.text() == str(observation.id):
+ """Update input info from an observation object"""
+ if self.selected_observation_id == observation.id:
return
- self.input_obs_id.set_id(observation.id)
- self.input_taxon_id.set_id(observation.taxon.id)
+ self.selected_taxon_id = None
+ self.selected_observation_id = observation.id
+ self.input_obs_id.clear()
+ self.input_taxon_id.clear()
self.data_source_card.clear()
+
card = ObservationInfoCard(obs=observation, delayed_load=False)
- card.on_click.connect(self.on_select_observation_tab)
+ card.on_click.connect(self.on_view_observation_id)
self.data_source_card.addWidget(card)
def info(self, message: str):
diff --git a/naturtag/controllers/image_gallery.py b/naturtag/controllers/image_gallery.py
index 936649d8..7a7962fb 100644
--- a/naturtag/controllers/image_gallery.py
+++ b/naturtag/controllers/image_gallery.py
@@ -49,8 +49,8 @@ class ImageGallery(BaseController):
"""Container for displaying local image thumbnails & info"""
on_load_images = Signal(list) #: New images have been loaded
- on_select_taxon = Signal(int) #: A taxon was selected from context menu
- on_select_observation = Signal(int) #: An observation was selected from context menu
+ on_view_taxon_id = Signal(int) #: A taxon was selected from context menu
+ on_view_observation_id = Signal(int) #: An observation was selected from context menu
def __init__(self):
super().__init__()
@@ -69,7 +69,6 @@ def __init__(self):
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll_area.setWidget(self.scroll_panel)
- root.addLayout(self.flow_layout)
root.addWidget(scroll_area)
def clear(self):
@@ -128,8 +127,8 @@ def _bind_image_actions(self, thumbnail: 'ThumbnailCard'):
thumbnail.on_remove.connect(self.remove_image)
thumbnail.on_select.connect(self.select_image)
thumbnail.on_copy.connect(self.on_message)
- thumbnail.context_menu.on_select_taxon.connect(self.on_select_taxon)
- thumbnail.context_menu.on_select_observation.connect(self.on_select_observation)
+ thumbnail.context_menu.on_view_taxon_id.connect(self.on_view_taxon_id)
+ thumbnail.context_menu.on_view_observation_id.connect(self.on_view_observation_id)
def dragEnterEvent(self, event):
event.acceptProposedAction()
@@ -314,8 +313,8 @@ def set_pixmap_meta(self, pixmap_meta: tuple[QPixmap, MetaMetadata]):
class ThumbnailContextMenu(QMenu):
"""Context menu for local image thumbnails"""
- on_select_taxon = Signal(int) #: A taxon was selected from context menu
- on_select_observation = Signal(int) #: An observation was selected from context menu
+ on_view_taxon_id = Signal(int) #: A taxon was selected from context menu
+ on_view_observation_id = Signal(int) #: An observation was selected from context menu
def refresh_actions(self, thumbnail_card: ThumbnailCard):
"""Update menu actions based on the available metadata"""
@@ -328,7 +327,7 @@ def refresh_actions(self, thumbnail_card: ThumbnailCard):
text='View Taxon',
tooltip=f'View taxon {meta.taxon_id} in naturtag',
enabled=meta.has_taxon,
- callback=lambda: self.on_select_taxon.emit(meta.taxon_id),
+ callback=lambda: self.on_view_taxon_id.emit(meta.taxon_id),
)
self._add_action(
parent=thumbnail_card,
@@ -344,7 +343,7 @@ def refresh_actions(self, thumbnail_card: ThumbnailCard):
text='View Observation',
tooltip=f'View observation {meta.observation_id} in naturtag',
enabled=meta.has_observation,
- callback=lambda: self.on_select_observation.emit(meta.observation_id),
+ callback=lambda: self.on_view_observation_id.emit(meta.observation_id),
)
self._add_action(
parent=thumbnail_card,
diff --git a/naturtag/controllers/observation_controller.py b/naturtag/controllers/observation_controller.py
index f3cd93e4..607150df 100644
--- a/naturtag/controllers/observation_controller.py
+++ b/naturtag/controllers/observation_controller.py
@@ -1,7 +1,7 @@
from logging import getLogger
from typing import Iterable
-from pyinaturalist import Observation
+from pyinaturalist import Observation, Taxon
from PySide6.QtCore import Qt, QThread, QTimer, Signal, Slot
from PySide6.QtWidgets import QLabel, QPushButton
@@ -14,13 +14,13 @@
class ObservationController(BaseController):
- on_select = Signal(Observation) #: An observation was selected
+ on_view_taxon = Signal(Taxon) #: Request to switch to taxon tab
def __init__(self):
super().__init__()
self.root = HorizontalLayout(self)
self.root.setAlignment(Qt.AlignLeft)
- self.selected_observation: Observation = None
+ self.displayed_observation: Observation = None
# Search inputs
# self.search = ObservationSearch(self.app.settings)
@@ -66,9 +66,8 @@ def __init__(self):
self.next_button.setEnabled(False)
button_layout.addWidget(self.next_button)
- # Selected observation info
+ # Full observation info viewer
self.obs_info = ObservationInfoSection()
- self.obs_info.on_select.connect(self.display_observation)
obs_layout = VerticalLayout()
obs_layout.addLayout(self.obs_info)
self.root.addLayout(obs_layout)
@@ -83,13 +82,13 @@ def __init__(self):
# Actions triggered directly by UI
# ----------------------------------------
- def select_observation(self, observation_id: int):
- """Select an observation to display full details"""
- # Don't need to do anything if this observation is already selected
- if self.selected_observation and self.selected_observation.id == observation_id:
+ def display_observation_by_id(self, observation_id: int):
+ """Display full observation details"""
+ # Don't need to do anything if this observation is already displayed
+ if self.displayed_observation and self.displayed_observation.id == observation_id:
return
- logger.info(f'Selecting observation {observation_id}')
+ logger.info(f'Loading observation {observation_id}')
future = self.app.threadpool.schedule(
lambda: self.app.client.observations(observation_id, taxonomy=True),
priority=QThread.HighPriority,
@@ -128,8 +127,7 @@ def refresh(self):
@Slot(Observation)
def display_observation(self, observation: Observation):
"""Display full details for a single observation"""
- self.selected_observation = observation
- self.on_select.emit(observation)
+ self.displayed_observation = observation
self.obs_info.load(observation)
logger.debug(f'Loaded observation {observation.id}')
@@ -145,7 +143,7 @@ def display_user_observations(self, observations: list[Observation]):
def bind_selection(self, obs_cards: Iterable[ObservationInfoCard]):
"""Connect click signal from each observation card"""
for obs_card in obs_cards:
- obs_card.on_click.connect(self.select_observation)
+ obs_card.on_click.connect(self.display_observation_by_id)
def update_pagination_buttons(self):
"""Update pagination buttons based on current page"""
@@ -164,7 +162,11 @@ def get_user_observations(self) -> list[Observation]:
# Maybe do that except on initial observation load?
self.total_results = self.app.client.observations.count(username=self.app.settings.username)
self.total_pages = (self.total_results // DEFAULT_PAGE_SIZE) + 1
- logger.debug('Total user observations: %s (%s pages)', self.total_results, self.total_pages)
+ logger.debug(
+ 'Total user observations: %s (%s pages)',
+ self.total_results,
+ self.total_pages,
+ )
observations = self.app.client.observations.get_user_observations(
username=self.app.settings.username,
diff --git a/naturtag/controllers/observation_view.py b/naturtag/controllers/observation_view.py
index 926608df..d1e470f1 100644
--- a/naturtag/controllers/observation_view.py
+++ b/naturtag/controllers/observation_view.py
@@ -6,7 +6,7 @@
from collections import deque
from logging import getLogger
-from pyinaturalist import Observation
+from pyinaturalist import Observation, Taxon
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtWidgets import QGroupBox, QLabel, QPushButton
@@ -29,7 +29,8 @@
class ObservationInfoSection(HorizontalLayout):
"""Section to display selected observation photos and info"""
- on_select = Signal(Observation) #: An observation was selected
+ on_select = Signal(Observation) #: An observation was selected for tagging
+ on_view_taxon = Signal(Taxon) #: A taxon was selected for viewing
def __init__(self):
super().__init__()
@@ -90,7 +91,24 @@ def __init__(self):
# self.parent_button.clicked.connect(self.select_parent)
# button_layout.addWidget(self.parent_button)
- # # Link button: Open web browser to taxon info page
+ # Select button: Use observation for tagging
+ self.select_button = QPushButton('Select')
+ self.select_button.setIcon(fa_icon('fa.tag', primary=True))
+ self.select_button.clicked.connect(lambda: self.on_select.emit(self.selected_observation))
+ self.select_button.setEnabled(False)
+ self.select_button.setToolTip('Select this observation for tagging')
+ button_layout.addWidget(self.select_button)
+
+ # View taxon button
+ self.view_taxon_button = QPushButton('View Taxon')
+ self.view_taxon_button.setIcon(fa_icon('fa5s.spider', primary=True))
+ self.view_taxon_button.clicked.connect(
+ lambda: self.on_view_taxon.emit(self.selected_observation.taxon)
+ )
+ self.view_taxon_button.setEnabled(False)
+ button_layout.addWidget(self.view_taxon_button)
+
+ # Link button: Open web browser to observation info page
self.link_button = QPushButton('View on iNaturalist')
self.link_button.setIcon(fa_icon('mdi.web', primary=True))
self.link_button.clicked.connect(lambda: webbrowser.open(self.selected_observation.uri))
@@ -127,7 +145,7 @@ def load(self, obs: Observation):
size='medium',
priority=QThread.HighPriority,
)
- self._update_nav_buttons()
+ self._update_buttons()
# Load additional thumbnails
self.thumbnails.clear()
@@ -169,7 +187,7 @@ def load(self, obs: Observation):
)
self.details.add_line(
GEOPRIVACY_ICONS.get(obs.geoprivacy, 'mdi.map-marker'),
- f'Coordinates: {obs.private_location or obs.location} '
+ f"Coordinates: {obs.private_location or obs.location} "
f'({obs.geoprivacy or "Unknown geoprivacy"})',
)
self.details.add_line(
@@ -191,12 +209,8 @@ def load(self, obs: Observation):
# self.hist_prev.append(self.selected_observation)
# self.on_select_obj.emit(self.history_taxon)
- def select_observation(self, observation: Observation):
- self.load(observation)
- self.on_select.emit(observation)
-
- def _update_nav_buttons(self):
- """Update status and tooltip for 'back', 'forward', 'parent', and 'view on iNat' buttons"""
+ def _update_buttons(self):
+ """Update status and tooltip for nav and selection buttons"""
# self.prev_button.setEnabled(bool(self.hist_prev))
# self.prev_button.setToolTip(self.hist_prev[-1].full_name if self.hist_prev else None)
# self.next_button.setEnabled(bool(self.hist_next))
@@ -207,3 +221,8 @@ def _update_nav_buttons(self):
# )
self.link_button.setEnabled(True)
self.link_button.setToolTip(self.selected_observation.uri)
+ self.view_taxon_button.setEnabled(True)
+ self.view_taxon_button.setToolTip(
+ f'See details for {self.selected_observation.taxon.full_name}'
+ )
+ self.select_button.setEnabled(True)
diff --git a/naturtag/controllers/taxon_controller.py b/naturtag/controllers/taxon_controller.py
index 2a3af12e..f97e7925 100644
--- a/naturtag/controllers/taxon_controller.py
+++ b/naturtag/controllers/taxon_controller.py
@@ -35,8 +35,9 @@ def __init__(self):
# Search inputs
self.search = TaxonSearch()
- self.search.autocomplete.on_select.connect(self.select_taxon)
+ self.search.autocomplete.on_select.connect(self.display_taxon_by_id)
self.search.on_results.connect(self.set_search_results)
+ self.search.iconic_taxon_filters.on_select.connect(self.display_taxon_by_id)
self.on_select.connect(self.search.set_taxon)
self.root.addLayout(self.search)
@@ -49,8 +50,8 @@ def __init__(self):
# Selected taxon info
self.taxon_info = TaxonInfoSection()
- self.taxon_info.on_select_id.connect(self.select_taxon)
- self.taxon_info.on_select.connect(self.display_taxon)
+ self.taxon_info.on_view_taxon_by_id.connect(self.display_taxon_by_id)
+ self.taxon_info.on_view_taxon.connect(self.display_taxon)
self.taxonomy = TaxonomySection(self.user_taxa)
taxon_layout = VerticalLayout()
taxon_layout.addLayout(self.taxon_info)
@@ -60,11 +61,12 @@ def __init__(self):
# Navigation keyboard shortcuts
self.add_shortcut('Ctrl+Left', self.taxon_info.prev)
self.add_shortcut('Ctrl+Right', self.taxon_info.next)
- self.add_shortcut('Alt+Up', self.taxon_info.select_parent)
+ self.add_shortcut('Ctrl+Up', self.taxon_info.select_parent)
self.add_shortcut('Ctrl+Shift+Enter', self.search.search)
self.add_shortcut('Ctrl+Shift+X', self.search.reset)
- def select_taxon(self, taxon_id: int):
+ @Slot(int)
+ def display_taxon_by_id(self, taxon_id: int):
"""Load a taxon by ID and update info display. Taxon API request will be sent from a
separate thread, return to main thread, and then display info will be loaded from a separate
thread.
@@ -74,7 +76,7 @@ def select_taxon(self, taxon_id: int):
return
# Fetch taxon record
- logger.info(f'Selecting taxon {taxon_id}')
+ logger.info(f'Loading taxon {taxon_id}')
client = self.app.client
if self.tabs._init_complete:
self.app.threadpool.cancel()
@@ -82,7 +84,7 @@ def select_taxon(self, taxon_id: int):
lambda: client.taxa(taxon_id, locale=self.app.settings.locale),
priority=QThread.HighPriority,
)
- future.on_result.connect(lambda taxon: self.display_taxon(taxon))
+ future.on_result.connect(self.display_taxon)
@Slot(Taxon)
def display_taxon(self, taxon: Taxon, notify: bool = True):
@@ -106,7 +108,7 @@ def set_search_results(self, taxa: list[Taxon]):
def bind_selection(self, taxon_cards: Iterable[TaxonInfoCard]):
"""Connect click signal from each taxon card"""
for taxon_card in taxon_cards:
- taxon_card.on_click.connect(self.select_taxon)
+ taxon_card.on_click.connect(self.display_taxon_by_id)
class TaxonTabs(QTabWidget):
diff --git a/naturtag/controllers/taxon_view.py b/naturtag/controllers/taxon_view.py
index 1c2eb057..bf890692 100644
--- a/naturtag/controllers/taxon_view.py
+++ b/naturtag/controllers/taxon_view.py
@@ -29,15 +29,20 @@
class TaxonInfoSection(HorizontalLayout):
"""Section to display selected taxon photo and basic info"""
- on_select = Signal(Taxon) #: A taxon object was selected (from nav or another screen)
- on_select_id = Signal(int) #: A taxon ID was selected (from 'parent' button)
+ on_select = Signal(Taxon) #: A taxon was selected for tagging
+ on_view_observations = Signal(Taxon) #: Request to switch to observations tab
+
+ # When selecting a taxon for viewing, a signal is sent to controller instead of handling here,
+ # since there are multiple sections to load (not just this class)
+ on_view_taxon = Signal(Taxon) #: A taxon was selected for viewing (from nav or another screen)
+ on_view_taxon_by_id = Signal(int) #: A taxon ID was selected for viewing (from 'parent' button)
def __init__(self):
super().__init__()
self.hist_prev: deque[Taxon] = deque() # Viewing history for current session only
self.hist_next: deque[Taxon] = deque() # Set when loading from history
self.history_taxon: Taxon = None # Set when loading from history, to avoid loop
- self.selected_taxon: Taxon = None
+ self.displayed_taxon: Taxon = None
self.group_box = QGroupBox('No taxon selected')
root = VerticalLayout(self.group_box)
@@ -47,7 +52,7 @@ def __init__(self):
self.setAlignment(Qt.AlignTop)
# Medium taxon default photo
- self.image = TaxonPhoto(hover_icon=True, hover_event=False) # Disabled until 1st load
+ self.image = TaxonPhoto(hover_icon=True, hover_event=False) # Disabled until first load
self.image.setFixedHeight(395) # Height of 5 thumbnails + spacing
self.image.setAlignment(Qt.AlignTop)
images.addWidget(self.image)
@@ -58,34 +63,59 @@ def __init__(self):
self.thumbnails.setAlignment(Qt.AlignTop)
images.addLayout(self.thumbnails)
- # Back and Forward buttons: We already have the full Taxon object
- button_layout = HorizontalLayout()
+ # Button layout
+ button_layout = VerticalLayout()
+ button_row_1 = HorizontalLayout()
+ button_row_2 = HorizontalLayout()
+ button_layout.addLayout(button_row_1)
+ button_layout.addLayout(button_row_2)
root.addLayout(button_layout)
+
+ # Back and Forward buttons: We already have the full Taxon object
self.prev_button = QPushButton('Back')
self.prev_button.setIcon(fa_icon('ei.chevron-left'))
self.prev_button.clicked.connect(self.prev)
self.prev_button.setEnabled(False)
- button_layout.addWidget(self.prev_button)
+ button_row_1.addWidget(self.prev_button)
self.next_button = QPushButton('Forward')
self.next_button.setIcon(fa_icon('ei.chevron-right'))
self.next_button.clicked.connect(self.next)
self.next_button.setEnabled(False)
- button_layout.addWidget(self.next_button)
+ button_row_1.addWidget(self.next_button)
- # Parent button: We need to fetch the full Taxon object, so just pass the ID
+ # Parent button: Full Taxon object isn't available, so just pass the ID
self.parent_button = QPushButton('Parent')
self.parent_button.setIcon(fa_icon('ei.chevron-up'))
self.parent_button.clicked.connect(self.select_parent)
self.parent_button.setEnabled(False)
- button_layout.addWidget(self.parent_button)
+ button_row_1.addWidget(self.parent_button)
+
+ # Select button: Use taxon for tagging
+ self.select_button = QPushButton('Select')
+ self.select_button.setIcon(fa_icon('fa.tag', primary=True))
+ self.select_button.clicked.connect(lambda: self.on_select.emit(self.displayed_taxon))
+ self.select_button.setEnabled(False)
+ self.select_button.setToolTip('Select this taxon for tagging')
+ button_row_2.addWidget(self.select_button)
+
+ # View observations button
+ # TODO: Observation filters
+ self.view_observations_button = QPushButton('View Observations')
+ self.view_observations_button.setIcon(fa_icon('fa5s.binoculars', primary=True))
+ self.view_observations_button.clicked.connect(
+ lambda: self.on_view_observations.emit(self.displayed_taxon.id)
+ )
+ self.view_observations_button.setEnabled(False)
+ self.select_button.setToolTip('View your observations of this taxon')
+ button_row_2.addWidget(self.view_observations_button)
# Link button: Open web browser to taxon info page
self.link_button = QPushButton('View on iNaturalist')
self.link_button.setIcon(fa_icon('mdi.web', primary=True))
- self.link_button.clicked.connect(lambda: webbrowser.open(self.selected_taxon.url))
+ self.link_button.clicked.connect(lambda: webbrowser.open(self.displayed_taxon.url))
self.link_button.setEnabled(False)
- button_layout.addWidget(self.link_button)
+ button_row_2.addWidget(self.link_button)
# Fullscreen image viewer
self.image_window = TaxonImageWindow()
@@ -93,12 +123,12 @@ def __init__(self):
def load(self, taxon: Taxon):
"""Load default photo + additional thumbnails"""
- if self.selected_taxon and taxon.id == self.selected_taxon.id:
+ if self.displayed_taxon and taxon.id == self.displayed_taxon.id:
return
# Append to nav history, unless we just loaded a taxon from history
- if self.selected_taxon and taxon.id != getattr(self.history_taxon, 'id', None):
- self.hist_prev.append(self.selected_taxon)
+ if self.displayed_taxon and taxon.id != getattr(self.history_taxon, 'id', None):
+ self.hist_prev.append(self.displayed_taxon)
self.hist_next.clear()
logger.debug(
f'Navigation: {" | ".join([t.name for t in self.hist_prev])} | [{taxon.name}] | '
@@ -107,7 +137,7 @@ def load(self, taxon: Taxon):
# Set title and main photo
self.history_taxon = None
- self.selected_taxon = taxon
+ self.displayed_taxon = taxon
self.group_box.setTitle(taxon.full_name)
self.image.hover_event = True
self.image.taxon = taxon
@@ -117,7 +147,7 @@ def load(self, taxon: Taxon):
size='medium',
priority=QThread.HighPriority,
)
- self._update_nav_buttons()
+ self._update_buttons()
# Load additional thumbnails
self.thumbnails.clear()
@@ -132,35 +162,33 @@ def prev(self):
if not self.hist_prev:
return
self.history_taxon = self.hist_prev.pop()
- self.hist_next.appendleft(self.selected_taxon)
- self.on_select.emit(self.history_taxon)
+ self.hist_next.appendleft(self.displayed_taxon)
+ self.on_view_taxon.emit(self.history_taxon)
def next(self):
if not self.hist_next:
return
self.history_taxon = self.hist_next.popleft()
- self.hist_prev.append(self.selected_taxon)
- self.on_select.emit(self.history_taxon)
-
- def select_taxon(self, taxon: Taxon):
- self.load(taxon)
- self.on_select.emit(taxon)
+ self.hist_prev.append(self.displayed_taxon)
+ self.on_view_taxon.emit(self.history_taxon)
def select_parent(self):
- self.on_select_id.emit(self.selected_taxon.parent_id)
+ self.on_view_taxon_by_id.emit(self.displayed_taxon.parent_id)
- def _update_nav_buttons(self):
- """Update status and tooltip for 'back', 'forward', 'parent', and 'view on iNat' buttons"""
+ def _update_buttons(self):
+ """Update status and tooltip for nav and selection buttons"""
self.prev_button.setEnabled(bool(self.hist_prev))
self.prev_button.setToolTip(self.hist_prev[-1].full_name if self.hist_prev else None)
self.next_button.setEnabled(bool(self.hist_next))
self.next_button.setToolTip(self.hist_next[0].full_name if self.hist_next else None)
- self.parent_button.setEnabled(bool(self.selected_taxon.parent))
+ self.parent_button.setEnabled(bool(self.displayed_taxon.parent))
self.parent_button.setToolTip(
- self.selected_taxon.parent.full_name if self.selected_taxon.parent else None
+ self.displayed_taxon.parent.full_name if self.displayed_taxon.parent else None
)
+ self.select_button.setEnabled(True)
+ # self.view_observations_button.setEnabled(True)
self.link_button.setEnabled(True)
- self.link_button.setToolTip(self.selected_taxon.url)
+ self.link_button.setToolTip(self.displayed_taxon.url)
class TaxonomySection(HorizontalLayout):
diff --git a/naturtag/widgets/inputs.py b/naturtag/widgets/inputs.py
index 94ad6fb9..9f6a97a1 100644
--- a/naturtag/widgets/inputs.py
+++ b/naturtag/widgets/inputs.py
@@ -31,7 +31,10 @@ def focusOutEvent(self, event: Optional[QEvent] = None):
def select(self):
if self.text():
- self.on_select.emit(int(self.text()))
+ self.on_select.emit(self.get_id())
+
+ def get_id(self) -> int:
+ return int(self.text()) if self.text() else 0
def set_id(self, id: int):
self.setText(str(id))
diff --git a/pyproject.toml b/pyproject.toml
index 3084d92f..4a5a4c69 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -113,7 +113,7 @@ fix = true
unsafe-fixes = true
line-length = 100
output-format = 'grouped'
-target-version = 'py310'
+target-version = 'py311'
[tool.ruff.lint]
select = ['B', 'C4', 'C90', 'E', 'F']