Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
136eda40ba | ||
|
|
465ec9837b | ||
|
|
2f5c434623 | ||
|
|
89b84d8479 | ||
|
|
6d19b91963 | ||
|
|
43a2c4f57b | ||
|
|
b780c6a116 | ||
|
|
67c98c7c0a | ||
|
|
5bdc851fa0 | ||
|
|
9b9ac36261 | ||
|
|
ceec9ecdc0 |
No files matched your search
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Gebogen lijnen
|
||||||
|
overview: Lijnen en pijlen krijgen buigpunten waardoor een vloeiende kromme door alle punten loopt. Bij een geselecteerde lijn staat tussen elk paar punten een spook-greep; slepen daarvan maakt een nieuw buigpunt, dubbelklikken op een buigpunt verwijdert het.
|
||||||
|
todos:
|
||||||
|
- id: model
|
||||||
|
content: "LineAnnotation: Bends-lijst, Catmull-Rom-naar-Bezier helper, Render/Bounds/HitTest/Move/Clone aanpassen, pijlpunt langs raaklijn"
|
||||||
|
status: completed
|
||||||
|
- id: handles
|
||||||
|
content: "Grepen: buigpunten + spook-grepen, BeginHandleDrag voegt buigpunt in, RemoveHandle; IsGhostHandle/RemoveHandle in Annotation"
|
||||||
|
status: completed
|
||||||
|
- id: surface
|
||||||
|
content: "AnnotationSurface: spook-grepen anders tekenen, echte grepen voorrang bij HitHandle"
|
||||||
|
status: completed
|
||||||
|
- id: editor
|
||||||
|
content: "EditorWindow: dubbelklik op buigpunt verwijdert het (met undo), hint-tekst bijwerken"
|
||||||
|
status: completed
|
||||||
|
isProject: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Gebogen lijnen en pijlen
|
||||||
|
|
||||||
|
## Gedrag
|
||||||
|
- Selecteer een lijn of pijl: je ziet de begin- en eindgreep, de buigpunten en, halverwege elk stuk van de kromme, een kleine halfdoorzichtige spook-greep.
|
||||||
|
- Sleep je een spook-greep, dan wordt die een echt buigpunt en volgt de lijn de muis (zoals in je screenshot).
|
||||||
|
- Dubbelklik op een buigpunt verwijdert het. Beide acties kun je ongedaan maken.
|
||||||
|
- De kromme loopt precies door alle punten (Catmull-Rom, omgezet naar kubische Beziers). De pijlpunt volgt de richting van het laatste stuk.
|
||||||
|
|
||||||
|
## Wijzigingen
|
||||||
|
|
||||||
|
**[src/Kniip/Editor/Annotations/LineAnnotation.cs](src/Kniip/Editor/Annotations/LineAnnotation.cs)**
|
||||||
|
- Nieuw: `List<Point> Bends`, met `Start + Bends + End` als doorlooppunten.
|
||||||
|
- Een helper bouwt de Bezier-segmenten. Voor `Bounds`, `HitTest` en de posities van de spook-grepen gebruikt hij bemonsterde punten (ongeveer 16 per segment). Zonder buigpunten blijft alles gelijk aan nu.
|
||||||
|
- `Render`: een `StreamGeometry` met `BezierTo`. Bij een pijl wordt de richting bepaald door `End` min het laatste controlepunt. Het laatste segment eindigt op het basispunt van de pijlpunt.
|
||||||
|
- Grepen: index 0 is `Start` en 1 is `End`, zodat bestaande code blijft werken. Daarna volgen de buigpunten en dan de spook-grepen (het Bezier-punt bij t=0,5 van elk segment).
|
||||||
|
- `BeginHandleDrag`: bij een spook-greep wordt een buigpunt op de juiste plek in `Bends` ingevoegd en het actieve buigpunt onthouden. `MoveHandle` verplaatst daarna dat punt.
|
||||||
|
- Met Shift blijft `SnapAngle` werken voor begin en eind, maar dan ten opzichte van het naastliggende punt.
|
||||||
|
- `Move` verschuift ook alle buigpunten. `Clone` maakt een diepe kopie van `Bends`, nodig voor ongedaan maken (zelfde patroon als in `HighlightAnnotation`).
|
||||||
|
|
||||||
|
**[src/Kniip/Editor/Annotations/Annotation.cs](src/Kniip/Editor/Annotations/Annotation.cs)**
|
||||||
|
- `public virtual bool IsGhostHandle(int index) => false;`
|
||||||
|
- `public virtual bool RemoveHandle(int index) => false;` (LineAnnotation verwijdert hiermee een buigpunt)
|
||||||
|
|
||||||
|
**[src/Kniip/Editor/AnnotationSurface.cs](src/Kniip/Editor/AnnotationSurface.cs)**
|
||||||
|
- `DrawSelection` tekent spook-grepen als kleinere, halfdoorzichtige cirkels. `HitHandle` controleert eerst de echte grepen en daarna pas de spook-grepen, zodat een echte greep voorrang krijgt als ze overlappen.
|
||||||
|
|
||||||
|
**[src/Kniip/Editor/EditorWindow.xaml.cs](src/Kniip/Editor/EditorWindow.xaml.cs)** (rond regel 452)
|
||||||
|
- In `Surface_MouseLeftButtonDown`: als op een greep geklikt wordt met `e.ClickCount == 2` en `_selected.RemoveHandle(handle)` slaagt, dan volgen `Commit(_pending)` en `InvalidateVisual`, en stopt de verwerking daar. Anders start het bestaande sleepgedrag.
|
||||||
|
- Optioneel: in `UpdateHint` een korte tip toevoegen voor lijnen ("Sleep het middenpunt om te buigen, dubbelklik om een punt te verwijderen").
|
||||||
|
|
||||||
|
## Niet nodig
|
||||||
|
Exporteren gaat via `Render` in [src/Kniip/Editor/DocumentRenderer.cs](src/Kniip/Editor/DocumentRenderer.cs) en werkt dus automatisch. Ook het aanmaken van lijnen (`UpdateCreating`) blijft ongewijzigd: nieuwe lijnen zijn recht.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
name: Schermafdruk vertraging
|
||||||
|
overview: "Tray-submenu \"Schermafdruk vertraging: Xs\" (0-60 s, slider plus getalveld) dat rechthoek- en vensterknipsels uitstelt, met een klein aftelvenster bij de muis dat met Esc te annuleren is."
|
||||||
|
todos:
|
||||||
|
- id: setting
|
||||||
|
content: CaptureDelaySeconds toevoegen aan AppSettings (0-60, standaard 0)
|
||||||
|
status: completed
|
||||||
|
- id: tray
|
||||||
|
content: Tray-submenu met TrackBar + NumericUpDown, SetCaptureDelay en CaptureDelayChanged in TrayIcon
|
||||||
|
status: completed
|
||||||
|
- id: countdown
|
||||||
|
content: CountdownWindow (niet-activerend, volgt muis, Elapsed/Cancelled) maken
|
||||||
|
status: completed
|
||||||
|
- id: app
|
||||||
|
content: BeginDelayedSnip in App.xaml.cs, Esc-binding tijdelijk, StartNewSnip onvertraagd, instelling opslaan
|
||||||
|
status: completed
|
||||||
|
- id: changelog
|
||||||
|
content: CHANGELOG.md bijwerken
|
||||||
|
status: completed
|
||||||
|
- id: release
|
||||||
|
content: CHANGELOG 1.1.5-item, build.ps1 draaien, commit/tag 1.1.5 en pushen
|
||||||
|
status: completed
|
||||||
|
isProject: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Schermafdruk vertraging (0-60 s)
|
||||||
|
|
||||||
|
## Gedrag
|
||||||
|
- In het tray-menu komt direct onder "Venster knippen" een item `Schermafdruk vertraging: 0s` met een submenu.
|
||||||
|
- In het submenu staan een slider (0-60) en een getalveld (`NumericUpDown`, 0-60) die met elkaar meelopen. De tekst van het hoofditem werkt meteen bij, bijvoorbeeld `Schermafdruk vertraging: 5s`.
|
||||||
|
- 0 betekent uit: dan werkt alles zoals nu.
|
||||||
|
- De vertraging geldt voor "Rechthoek knippen" en "Venster knippen", via de tray en via de sneltoetsen. Screencasts en "Nieuw knipsel" na het sluiten van de editor (`StartNewSnip`) gaan zonder vertraging.
|
||||||
|
- Tijdens het aftellen staat een klein, altijd-bovenop, niet-focusstelend venstertje bij de muis met de resterende seconden. Het scherm wordt pas vastgelegd nadat dat venster weg is, zodat het niet in de schermafdruk komt. Met Esc (of een klik op het venster) annuleer je.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
trigger["Tray of sneltoets"] --> check{"Delay groter dan 0?"}
|
||||||
|
check -->|nee| snip["BeginSnip: scherm bevriezen"]
|
||||||
|
check -->|ja| countdown["CountdownWindow bij muis"]
|
||||||
|
countdown -->|"Esc of klik"| cancel[Geannuleerd]
|
||||||
|
countdown -->|"tijd om"| closeWin["Venster sluiten, korte pauze"] --> snip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wijzigingen
|
||||||
|
|
||||||
|
**[src/Kniip/Settings/AppSettings.cs](src/Kniip/Settings/AppSettings.cs)**
|
||||||
|
- Nieuwe instelling `public int CaptureDelaySeconds { get; set; }` (standaard 0), met een doc-comment in dezelfde stijl. Bij het lezen begrenzen tot 0-60.
|
||||||
|
|
||||||
|
**[src/Kniip/Infrastructure/TrayIcon.cs](src/Kniip/Infrastructure/TrayIcon.cs)**
|
||||||
|
- `_delayItem` (een `ToolStripMenuItem`) met in de `DropDownItems` een `ToolStripControlHost` om een klein `Panel`: daarin een `TrackBar` (0-60, `TickFrequency` 5) en een `NumericUpDown` met het label "seconden".
|
||||||
|
- `public void SetCaptureDelay(int seconds)` zet de besturingselementen en de menutekst.
|
||||||
|
- `public event Action<int>? CaptureDelayChanged`: wordt afgevuurd als de waarde wijzigt. Opslaan gebeurt pas bij `DropDownClosed`, zodat niet elke stap van de slider een schrijfactie wordt.
|
||||||
|
- Toetsen in het getalveld mogen het menu niet sluiten. Als dat in de praktijk toch gebeurt, krijgt het hostpaneel focus en geven we Enter/Esc expliciet door.
|
||||||
|
|
||||||
|
**[src/Kniip/App.xaml.cs](src/Kniip/App.xaml.cs)**
|
||||||
|
- Bij het opstarten: `_tray.SetCaptureDelay(Settings.CaptureDelaySeconds)` en `CaptureDelayChanged` koppelen. De koppeling zet `Settings.CaptureDelaySeconds` en roept `SettingsStore.Save` aan.
|
||||||
|
- `BeginRegionSnip`/`BeginWindowSnip` gaan via een nieuwe `BeginDelayedSnip(bool pickWindow)`:
|
||||||
|
- Als de vertraging 0 is, of er al een knipsel loopt, of er al wordt afgeteld, gaat het direct naar `BeginSnip`. Een tweede sneltoets tijdens het aftellen wordt genegeerd.
|
||||||
|
- Anders wordt een `CountdownWindow` gestart. Na afloop sluit dat venster en volgt een `DispatcherTimer` van circa 150 ms (zelfde idee als `StartNewSnip`), daarna `BeginSnip(pickWindow)`.
|
||||||
|
- Esc tijdens het aftellen: tijdelijk een `Escape`-binding toevoegen aan `HotkeyHook.Bindings` die het aftellen annuleert, en daarna via `ApplyHotkeys()` herstellen. `HotkeyHook.Capture` gebruiken we niet, want dat slokt alle toetsen op, en de gebruiker moet tijdens het aftellen juist zijn scherm kunnen klaarzetten.
|
||||||
|
- `StartNewSnip` blijft `BeginRegionSnip` direct aanroepen, maar dan via de onvertraagde route (`BeginSnip(false)`).
|
||||||
|
|
||||||
|
**Nieuw: `src/Kniip/Capture/CountdownWindow.xaml(.cs)`**
|
||||||
|
- Een klein, rond-hoekig WPF-venster zonder rand: `Topmost`, `ShowActivated=False`, `ShowInTaskbar=False`, met `WS_EX_NOACTIVATE`/`WS_EX_TOOLWINDOW` zodat de focus bij het actieve programma blijft. De opmaak is vergelijkbaar met [`SnipToast.xaml`](src/Kniip/Notifications/SnipToast.xaml).
|
||||||
|
- Toont een groot getal en de tekst "Esc om te annuleren". Er loopt een `DispatcherTimer` van 1 s. Het venster schuift mee met de muis (positie bijwerken op dezelfde tick of op een snellere tick van circa 50 ms).
|
||||||
|
- Events `Elapsed` en `Cancelled`. Een muisklik op het venster telt als annuleren.
|
||||||
|
|
||||||
|
## Release 1.1.5
|
||||||
|
De versienummers in [build.ps1](build.ps1), [installer/Kniip.iss](installer/Kniip.iss), [src/Kniip/Kniip.csproj](src/Kniip/Kniip.csproj) en [README.md](README.md) staan al op 1.1.5. Tag `1.1.5` bestaat nog niet. Deze functie gaat dus mee in dezelfde release als de ShareX-indeling, het vergrootglas en de geschiedenis.
|
||||||
|
- [CHANGELOG.md](CHANGELOG.md): in de bestaande sectie `## 1.1.5 - 2026-11-09` onder **Nieuw** een item toevoegen in dezelfde stijl, bijvoorbeeld: **Schermafdruk vertraging.** In het systeemvakmenu staat **Schermafdruk vertraging: 0s**. Stel daar met de schuifbalk of het getalveld 0 tot 60 seconden in. Daarna wachten **Rechthoek knippen** en **Venster knippen** (ook via de sneltoets) zo lang voordat het scherm bevriest. Bij de muis telt een venstertje af, en met Esc annuleer je. 0 betekent geen vertraging; screencasts wachten niet.
|
||||||
|
- `.\build.ps1` draaien en controleren dat de build en de installer slagen.
|
||||||
|
- Commit "1.1.5" met alle openstaande 1.1.5-wijzigingen, waaronder de nieuwe History- en Magnifier-bestanden. Daarna tag `1.1.5` en pushen, net als bij 1.1.4. De release op git.hibbit.net maak je zelf aan; ik lever de changelogtekst aan.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: ShareX-indeling editor
|
||||||
|
overview: "Een tweede editor-indeling toevoegen in de stijl van ShareX: bovenaan de bewerkgereedschappen met daaronder de eigenschappenbalk, onderaan een aparte balk met zoom en de opslag-, upload- en kopieerknoppen. De indeling is te kiezen in Instellingen (tab Editor) en via het rechtsklikmenu van de werkbalk. Uitbrengen als versie 1.1.5."
|
||||||
|
todos:
|
||||||
|
- id: settings
|
||||||
|
content: EditorLayout-enum, EditorLayout- en SplitToolbarOrder-instelling toevoegen aan AppSettings
|
||||||
|
status: completed
|
||||||
|
- id: xaml
|
||||||
|
content: ActionBar met ActionItems (ToolbarPanel) toevoegen aan EditorWindow.xaml
|
||||||
|
status: completed
|
||||||
|
- id: code
|
||||||
|
content: "Werkbalkcode ombouwen: items verdelen over twee panelen, labels/vergrendelen/verbergen/volgorde/omwisselen per paneel, menu Indeling, bijsnijden, OnActivated"
|
||||||
|
status: completed
|
||||||
|
- id: settingsui
|
||||||
|
content: Radioknoppen voor de indeling in de Editor-tab van Instellingen plus herstellen naar standaard
|
||||||
|
status: completed
|
||||||
|
- id: version
|
||||||
|
content: Versie 1.1.5 in csproj, build.ps1 en Kniip.iss en een CHANGELOG-regel
|
||||||
|
status: completed
|
||||||
|
- id: verify
|
||||||
|
content: Bouwen en beide indelingen handmatig testen
|
||||||
|
status: completed
|
||||||
|
isProject: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# ShareX-achtige editor-indeling (versie 1.1.5)
|
||||||
|
|
||||||
|
## Indeling
|
||||||
|
|
||||||
|
- **Eén werkbalk** (standaard, zoals nu): alles in `ToolbarItems`, positie Boven/Onder/Links/Rechts blijft werken.
|
||||||
|
- **Gesplitst (zoals ShareX)**:
|
||||||
|
- Boven, gecentreerd: Selecteren, Rechthoek ... Vervagen, Bijsnijden, met daarnaast Wissen, Ongedaan maken, Opnieuw en Verwijderen. Direct daaronder de bestaande eigenschappenbalk `PropsBar` (die al gecentreerd is).
|
||||||
|
- Onder, boven de statusbalk: links Nieuw en Screencast, in het midden Uitzoomen / 100% / Inzoomen, rechts Instellingen, Opslaan, Uploaden en Kopiëren (met tekstlabels, want daar is ruimte).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
topBar["Boven: gereedschappen + ongedaan maken"]
|
||||||
|
props["Eigenschappenbalk: kleur, dikte, lijnstijl"]
|
||||||
|
canvas["Canvas"]
|
||||||
|
bottomBar["Onder: Nieuw, Screencast | Zoom | Instellingen, Opslaan, Uploaden, Kopiëren"]
|
||||||
|
status["Statusbalk"]
|
||||||
|
topBar --> props --> canvas --> bottomBar --> status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wijzigingen
|
||||||
|
|
||||||
|
**[src/Kniip/Settings/AppSettings.cs](src/Kniip/Settings/AppSettings.cs)**
|
||||||
|
- Nieuwe enum `EditorLayout { Single, Split }` en `public EditorLayout EditorLayout { get; set; } = EditorLayout.Single;`
|
||||||
|
- Aparte `List<string> SplitToolbarOrder`, zodat versleepte volgordes van beide indelingen elkaar niet overschrijven. `ToolbarHidden` blijft gedeeld.
|
||||||
|
|
||||||
|
**[src/Kniip/Editor/EditorWindow.xaml](src/Kniip/Editor/EditorWindow.xaml)**
|
||||||
|
- Een tweede `Border x:Name="ActionBar" DockPanel.Dock="Bottom"` (na de statusbalk gedeclareerd, dus erboven) met `local:ToolbarPanel x:Name="ActionItems"`, dezelfde `ContextMenuOpening`- en `OrderChanged`-handlers en standaard `Collapsed`.
|
||||||
|
- De knoppen zelf blijven op één plek in de XAML staan; de code verdeelt ze over de panelen.
|
||||||
|
|
||||||
|
**[src/Kniip/Editor/EditorWindow.xaml.cs](src/Kniip/Editor/EditorWindow.xaml.cs)** (rond regels 141-362 voor de werkbalk)
|
||||||
|
- In `InitializeToolbar` alle items één keer in een dictionary `_toolbarItems` (id naar element) vastleggen, plus een vaste set id's die in de gesplitste indeling onderaan komen: `New, Screencast, Spacer, ZoomOut, Zoom, ZoomIn, Spacer2, Settings, Save, Upload, Copy` (plus de bijbehorende scheidingslijnen).
|
||||||
|
- `ApplyToolbarOrder` ombouwen tot `ApplyToolbarLayout()`: beide panelen leegmaken en de items volgens de opgeslagen volgorde (`ToolbarOrder` of `SplitToolbarOrder`) verdelen over `ToolbarItems` en `ActionItems`. De logica voor nieuwe items die na hun standaardvoorganger komen, blijft hetzelfde. In de gesplitste indeling krijgt het bovenste paneel `HorizontalAlignment=Center`, `Toolbar` vast `Dock.Top` en wordt `ActionBar` getoond.
|
||||||
|
- Alle plekken die nu `ToolbarItems.Items` gebruiken, laten werken op beide panelen: `SelectTool` (regel 368), "Knoppen tonen", `ResetToolbar`, de vergrendeling (`IsLocked` op beide panelen, achtergrondkleur op beide balken) en `OrderChanged` (de volgorde van boven + onder samen opslaan in `SplitToolbarOrder`).
|
||||||
|
- `UpdateToolbarLayout` en `SetActionLabelsVisible` per paneel laten rekenen, zodat de labels op de onderbalk zichtbaar blijven, ook als de bovenbalk vol is. De lus op regel 236 stopt bij het `ToolbarPanel` waarin het label staat, in plaats van alleen bij `ToolbarItems`.
|
||||||
|
- `SwapToolbarGroups` werkt op het paneel waarop is rechtsgeklikt.
|
||||||
|
- Rechtsklikmenu: een nieuw submenu "Indeling" met "Eén werkbalk" en "Gesplitst (zoals ShareX)". "Positie" alleen tonen bij Eén werkbalk.
|
||||||
|
- Op regel 935, waar `Toolbar` tijdens bijsnijden wordt verborgen, ook `ActionBar` verbergen (alleen als de gesplitste indeling aanstaat).
|
||||||
|
- In `OnActivated` (regel 1508) `ApplyToolbarLayout()` aanroepen als de indeling is veranderd, zodat een wijziging in Instellingen meteen zichtbaar is in een open editor (zoals dat nu ook gebeurt met `ApplySaveButton`).
|
||||||
|
|
||||||
|
**[src/Kniip/Settings/SettingsWindow.xaml](src/Kniip/Settings/SettingsWindow.xaml) en [.xaml.cs](src/Kniip/Settings/SettingsWindow.xaml.cs)**
|
||||||
|
- In de tab Editor (regel 252) een kopje "Werkbalk" met twee radioknoppen `LayoutSingleRadio` / `LayoutSplitRadio` en een korte uitleg, op dezelfde manier geladen en opgeslagen als `SaveOnlyRadio` / `SaveAndCopyRadio`.
|
||||||
|
- Bij het herstellen van de standaardinstellingen (regel 466) ook `EditorLayout` en `SplitToolbarOrder` meenemen.
|
||||||
|
|
||||||
|
**Versie 1.1.5**
|
||||||
|
- `<Version>1.1.5</Version>` in [src/Kniip/Kniip.csproj](src/Kniip/Kniip.csproj), de standaardwaarde in [build.ps1](build.ps1) (regel 16) en `#define AppVersion` in [installer/Kniip.iss](installer/Kniip.iss).
|
||||||
|
- In [CHANGELOG.md](CHANGELOG.md) bovenaan `## 1.1.5 - 2026-11-09`, onder `### Nieuw`: **Werkbalkindeling zoals ShareX.**
|
||||||
|
|
||||||
|
## Controle
|
||||||
|
- Bouwen met `build.ps1`, daarna beide indelingen uitproberen: wisselen via Instellingen en via het rechtsklikmenu, knoppen verslepen en verbergen, bijsnijden, een smal venster (de balken moeten netjes doorlopen naar een nieuwe regel) en "Standaard herstellen".
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
name: Update-melding blauwe stip
|
||||||
|
overview: kniip controleert op de achtergrond (bij het starten en daarna elke 24 uur, uit te zetten in Instellingen) op een nieuwe versie. Als die er is, krijgen de knop Instellingen in de editor-werkbalk en de tab Update in Instellingen een blauwe stip.
|
||||||
|
todos:
|
||||||
|
- id: setting
|
||||||
|
content: CheckForUpdates toevoegen aan AppSettings
|
||||||
|
status: completed
|
||||||
|
- id: notifier
|
||||||
|
content: UpdateNotifier maken (controle na het starten en elke 24 uur, AvailabilityChanged-event, Report)
|
||||||
|
status: completed
|
||||||
|
- id: app
|
||||||
|
content: "UpdateNotifier koppelen in App: aanroepen bij het opstarten en in SaveSettings"
|
||||||
|
status: completed
|
||||||
|
- id: editor-badge
|
||||||
|
content: Blauwe stip op de knop Instellingen in EditorWindow
|
||||||
|
status: completed
|
||||||
|
- id: settings-tab
|
||||||
|
content: Blauwe stip op de tab Update, vinkje voor automatische controle, Report na handmatige controle
|
||||||
|
status: completed
|
||||||
|
- id: changelog-readme
|
||||||
|
content: CHANGELOG.md sectie 1.1.4 toevoegen en README bijwerken (achtergrondcontrole, versietabel, build-commentaar)
|
||||||
|
status: completed
|
||||||
|
- id: bump-version
|
||||||
|
content: Versie naar 1.1.4 in build.ps1, installer/Kniip.iss en src/Kniip/Kniip.csproj
|
||||||
|
status: completed
|
||||||
|
- id: build-release
|
||||||
|
content: build.ps1 draaien, commit "1.1.4", tag 1.1.4 en pushen; installer klaarzetten voor de release op git.hibbit.net
|
||||||
|
status: completed
|
||||||
|
isProject: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Blauwe update-melding bij Instellingen en de tab Update
|
||||||
|
|
||||||
|
## Huidige situatie
|
||||||
|
- [src/Kniip/Update/UpdateChecker.cs](src/Kniip/Update/UpdateChecker.cs) haalt releases op met `GetReleasesAsync`, maar dat gebeurt nu alleen in [src/Kniip/Settings/SettingsWindow.xaml.cs](src/Kniip/Settings/SettingsWindow.xaml.cs) zodra de tab Update geopend wordt (`Tabs_SelectionChanged` gevolgd door `CheckForUpdatesAsync`).
|
||||||
|
- De knop Instellingen staat in [src/Kniip/Editor/EditorWindow.xaml](src/Kniip/Editor/EditorWindow.xaml) (regel 318–321, glyph ``).
|
||||||
|
- `AccentBrush` is `#0067C0` (blauw) in [src/Kniip/App.xaml](src/Kniip/App.xaml), en die gebruiken we voor de stip.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
AppStart[App start] --> Notifier[UpdateNotifier]
|
||||||
|
Timer["Timer 24 uur"] --> Notifier
|
||||||
|
Notifier -->|GetReleasesAsync| Checker[UpdateChecker]
|
||||||
|
Notifier -->|AvailabilityChanged| EditorBadge[Stip op knop Instellingen]
|
||||||
|
Notifier -->|AvailabilityChanged| TabBadge[Stip op tab Update]
|
||||||
|
SettingsCheck[Handmatige controle in tab Update] -->|Report| Notifier
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stappen
|
||||||
|
|
||||||
|
1. **Nieuwe instelling** in [src/Kniip/Settings/AppSettings.cs](src/Kniip/Settings/AppSettings.cs):
|
||||||
|
`public bool CheckForUpdates { get; set; } = true;`
|
||||||
|
|
||||||
|
2. **Nieuwe klasse `UpdateNotifier`** in `src/Kniip/Update/UpdateNotifier.cs`:
|
||||||
|
- Houdt `ReleaseInfo? Available` bij en `bool IsUpdateAvailable`, met een event `AvailabilityChanged` dat op de UI-thread wordt afgevuurd.
|
||||||
|
- `Apply(bool enabled)`: als de controle aanstaat, wordt die kort na het starten uitgevoerd (ongeveer 10 seconden vertraging, zodat het opstarten niet trager wordt) en daarna via een `DispatcherTimer` elke 24 uur. Als de controle uitstaat, stopt de timer en verdwijnt de stip.
|
||||||
|
- `CheckAsync()` roept `UpdateChecker.GetReleasesAsync` aan en vangt `UpdateException` stil af, zodat een achtergrondcontrole nooit een foutmelding toont.
|
||||||
|
- `Report(ReleaseInfo? newest)` wordt gebruikt door de handmatige controle in de tab Update, zodat de stip ook daarmee gelijkloopt.
|
||||||
|
- "Nieuwer" betekent hetzelfde als in `ShowReleases`: `r.Version > UpdateChecker.CurrentVersion`.
|
||||||
|
|
||||||
|
3. **App** ([src/Kniip/App.xaml.cs](src/Kniip/App.xaml.cs)):
|
||||||
|
- Een property `public UpdateNotifier Updates { get; } = new();`
|
||||||
|
- Bij het opstarten (na `ApplyHotkeys`) wordt `Updates.Apply(Settings.CheckForUpdates)` aangeroepen.
|
||||||
|
- In `SaveSettings()` gebeurt dat opnieuw, zodat het aan- of uitzetten van de controle direct effect heeft.
|
||||||
|
|
||||||
|
4. **Stip op de knop Instellingen** ([src/Kniip/Editor/EditorWindow.xaml](src/Kniip/Editor/EditorWindow.xaml) en `.xaml.cs`):
|
||||||
|
- De inhoud van de knop wordt een `Grid` met de bestaande glyph plus `<Ellipse x:Name="SettingsBadge" Width="8" Height="8" Fill="{StaticResource AccentBrush}" Stroke="White" StrokeThickness="1" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,-2,-3,0" Visibility="Collapsed" />`.
|
||||||
|
- In de code-behind abonneert de editor zich op `App.Instance.Updates.AvailabilityChanged`. Bij een wijziging worden `SettingsBadge.Visibility` en de tooltip bijgewerkt (de tooltip wordt "Instellingen - nieuwe versie beschikbaar"). Bij `Closed` wordt het abonnement weer opgezegd.
|
||||||
|
|
||||||
|
5. **Stip op de tab Update en een vinkje voor de achtergrondcontrole** ([src/Kniip/Settings/SettingsWindow.xaml](src/Kniip/Settings/SettingsWindow.xaml) en `.xaml.cs`):
|
||||||
|
- De `Header` van `UpdateTab` wordt een horizontale `StackPanel` met de tekst "Update" en daarachter een blauwe `Ellipse x:Name="UpdateTabBadge"` van dezelfde stijl, met een margin links van 6.
|
||||||
|
- In de tab Update, onder `CurrentVersionText`, komt `<CheckBox x:Name="AutoUpdateCheckBox" Content="Automatisch controleren op nieuwe versies (bij starten en dagelijks)" />`. Die wordt gevuld in `LoadFrom`, bewaard in `Save_Click` en meegenomen bij Standaardinstellingen.
|
||||||
|
- Het venster abonneert zich op `AvailabilityChanged` om de zichtbaarheid van de stip te zetten, en zegt dat abonnement op bij `Closed`.
|
||||||
|
- Na een handmatige controle roept `ShowReleases` `App.Instance.Updates.Report(newer.FirstOrDefault())` aan.
|
||||||
|
|
||||||
|
## Gedrag
|
||||||
|
- De stip blijft zichtbaar zolang er een nieuwere versie is. Na het updaten start kniip met de nieuwe versie, en dan is er geen nieuwere versie meer, dus verdwijnt de stip vanzelf.
|
||||||
|
- Er komen geen extra meldingen in het systeemvak, zoals afgesproken.
|
||||||
|
|
||||||
|
## Changelog en release 1.1.4 (na de codewijzigingen)
|
||||||
|
|
||||||
|
6. **[CHANGELOG.md](CHANGELOG.md)**: bovenaan een nieuwe sectie `## 1.1.4 - 2026-11-09` in dezelfde stijl als de eerdere versies, bijvoorbeeld:
|
||||||
|
- Onder `### Nieuw`: **Melding bij een nieuwe versie.** kniip kijkt bij het starten en daarna elke dag of er een nieuwere versie is. Als die er is, staat er een blauwe stip op de knop **Instellingen** in de editor en op de tab **Update**. Je zet dit uit in de tab **Update** met **Automatisch controleren op nieuwe versies**.
|
||||||
|
- Onder `### Gewijzigd`: kniip zoekt nu standaard zelf op de achtergrond naar updates. Tot nu toe gebeurde dat alleen als je de tab **Update** opende, zoals vermeld bij 1.1.0.
|
||||||
|
7. **[README.md](README.md)**:
|
||||||
|
- In de versietabel 1.1.3 vervangen door 1.1.4, met de link `.../releases/download/1.1.4/Kniip-Setup-1.1.4.exe`.
|
||||||
|
- Het commentaar `# versie 1.1.3` bij het bouwen wordt `# versie 1.1.4`.
|
||||||
|
- Bij "Updaten vanuit kniip" komt een zin over de automatische controle en de blauwe stip.
|
||||||
|
8. **Versienummer** van 1.1.3 naar 1.1.4 in [build.ps1](build.ps1) (`$Version`), [installer/Kniip.iss](installer/Kniip.iss) (`#define AppVersion`) en [src/Kniip/Kniip.csproj](src/Kniip/Kniip.csproj) (`<Version>`).
|
||||||
|
9. **Bouwen en uitbrengen**, op dezelfde manier als bij 1.1.2 en 1.1.3:
|
||||||
|
- `.\build.ps1` draaien. Dat levert `artifacts\Kniip-1.1.4.exe` en `artifacts\Kniip-Setup-1.1.4.exe` op.
|
||||||
|
- Daarna `git add -A`, `git commit -m "1.1.4"`, `git tag 1.1.4` (zonder `v`, net als de vorige tags) en `git push origin HEAD` plus `git push origin 1.1.4`.
|
||||||
|
- De release op git.hibbit.net maak je, net als eerder, zelf aan. Je hangt daar `artifacts\Kniip-Setup-1.1.4.exe` aan als bijlage en plakt de 1.1.4-tekst uit de changelog in de beschrijving. Ik lever die tekst kant-en-klaar aan.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: Vergrootglas bij selectie
|
||||||
|
overview: Add a magnifier (circular zoom with crosshair and X/Y coordinates near the cursor) to the snip overlay and the screencast region selector, with a settings checkbox that is on by default. Release as 1.1.5.
|
||||||
|
todos:
|
||||||
|
- id: setting
|
||||||
|
content: ShowMagnifier (default true) toevoegen aan AppSettings
|
||||||
|
status: completed
|
||||||
|
- id: control
|
||||||
|
content: "MagnifierControl maken: ronde zoom, raster, kruisdraad, X/Y-badge, plaatsing bij cursor met randcorrectie"
|
||||||
|
status: completed
|
||||||
|
- id: snip
|
||||||
|
content: Vergrootglas in RegionCaptureWindow + doorgeven vanuit App.BeginSnip
|
||||||
|
status: completed
|
||||||
|
- id: screencast
|
||||||
|
content: Vergrootglas in RegionSelectorWindow met vooraf vastgelegd beeld vanuit RecordingController.BeginSelection
|
||||||
|
status: completed
|
||||||
|
- id: settings-ui
|
||||||
|
content: Checkbox in SettingsWindow (tab Sneltoetsen) laden en opslaan
|
||||||
|
status: completed
|
||||||
|
- id: release
|
||||||
|
content: CHANGELOG 1.1.5, README, versienummers, build.ps1 draaien, commit/tag 1.1.5 en pushen
|
||||||
|
status: completed
|
||||||
|
isProject: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vergrootglas bij het kiezen van een gebied (1.1.5)
|
||||||
|
|
||||||
|
## Starting point
|
||||||
|
- kniip has no magnifier yet. The snip overlay [src/Kniip/Capture/RegionCaptureWindow.xaml](src/Kniip/Capture/RegionCaptureWindow.xaml) shows a frozen screenshot (`Frozen`), a dim layer, the selection and a `W × H` badge.
|
||||||
|
- The screencast selector [src/Kniip/Recording/RegionSelectorWindow.xaml](src/Kniip/Recording/RegionSelectorWindow.xaml) is a transparent overlay over the live screen. It has no image to zoom into.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Settings["AppSettings.ShowMagnifier"] --> App["App.BeginSnip"]
|
||||||
|
Settings --> Rec["RecordingController.BeginSelection"]
|
||||||
|
App -->|"frozen image + flag"| Snip[RegionCaptureWindow]
|
||||||
|
Rec -->|"capture per screen + flag"| Sel[RegionSelectorWindow]
|
||||||
|
Snip --> Mag[MagnifierControl]
|
||||||
|
Sel --> Mag
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Setting**: in [src/Kniip/Settings/AppSettings.cs](src/Kniip/Settings/AppSettings.cs) add
|
||||||
|
`public bool ShowMagnifier { get; set; } = true;` with a Dutch doc comment.
|
||||||
|
2. **Shared control**: new `src/Kniip/Capture/MagnifierControl.xaml(.cs)` (UserControl, `IsHitTestVisible="False"`), styled like the screenshot:
|
||||||
|
- A round clipped area (about 130 DIP) showing an `Image` of the frozen bitmap with `NearestNeighbor` scaling. A `CroppedBitmap` of about 15x15 physical pixels around the cursor is enlarged around 8x.
|
||||||
|
- A pixel grid, a blue crosshair through the center pixel and a small square on the center pixel. The crosshair uses `AccentBrush`, the ring is white with a shadow.
|
||||||
|
- Below the circle, a badge in the same style as `SizeBadge` with `X: … Y: …` in physical screen coordinates.
|
||||||
|
- Method `Update(BitmapSource frozen, System.Drawing.Point physicalRelative, System.Drawing.Point screenPoint)`. Pixels outside the image are left black.
|
||||||
|
- Placement: bottom-right of the cursor at about 20 DIP offset. It flips left and/or up near the screen edge.
|
||||||
|
3. **Snip overlay** [src/Kniip/Capture/RegionCaptureWindow.xaml.cs](src/Kniip/Capture/RegionCaptureWindow.xaml.cs):
|
||||||
|
- Add a constructor parameter `bool showMagnifier` and add `<local:MagnifierControl x:Name="Magnifier" Visibility="Collapsed"/>` to the Grid.
|
||||||
|
- In `OnMouseMove`, update and show the magnifier while it is on (before and during dragging). Not in window-pick mode (`PicksWindow`), because exact pixels don't matter there. Hide it on `MouseLeave`.
|
||||||
|
- [src/Kniip/App.xaml.cs](src/Kniip/App.xaml.cs) `BeginSnip` passes `Settings.ShowMagnifier`.
|
||||||
|
4. **Screencast selector** [src/Kniip/Recording/RegionSelectorWindow.xaml.cs](src/Kniip/Recording/RegionSelectorWindow.xaml.cs):
|
||||||
|
- Add an optional `BitmapSource? frozen` constructor parameter. When it is set, the same magnifier runs in `OnMouseMove` (also while hovering over windows, because you can drag from there).
|
||||||
|
- [src/Kniip/Recording/RecordingController.cs](src/Kniip/Recording/RecordingController.cs) `BeginSelection`: if `_settings.ShowMagnifier` is on, call `ScreenGrabber.Capture(screen.Bounds)` per screen *before* the selectors are shown, so the overlay is not in the image. Otherwise pass `null`.
|
||||||
|
5. **Settings screen** [src/Kniip/Settings/SettingsWindow.xaml](src/Kniip/Settings/SettingsWindow.xaml) tab **Sneltoetsen**: below the hotkeys, add a card title "Gebied kiezen" with
|
||||||
|
`<CheckBox x:Name="MagnifierBox" Content="Vergrootglas tonen bij het kiezen van een gebied (knipsel en screencast)" />`.
|
||||||
|
In [SettingsWindow.xaml.cs](src/Kniip/Settings/SettingsWindow.xaml.cs), add a line to `LoadFrom` (around line 100) and to the save step (around line 444).
|
||||||
|
6. **Release 1.1.5**:
|
||||||
|
- [CHANGELOG.md](CHANGELOG.md) gets a new section `## 1.1.5 - 2026-11-09` with a **Nieuw** entry for the magnifier (round zoom with crosshair and X/Y, for both snips and screencasts) and how to turn it off.
|
||||||
|
- [README.md](README.md): the version table and build comment go to 1.1.5.
|
||||||
|
- Version 1.1.4 becomes 1.1.5 in [build.ps1](build.ps1), [installer/Kniip.iss](installer/Kniip.iss) and [src/Kniip/Kniip.csproj](src/Kniip/Kniip.csproj).
|
||||||
|
- Run `.\build.ps1`, then commit "1.1.5", tag `1.1.5` and push, the same way as 1.1.4. You create the release on git.hibbit.net yourself; I'll supply the changelog text.
|
||||||
+103
-4
@@ -2,11 +2,107 @@
|
|||||||
|
|
||||||
Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste versie staat bovenaan.
|
Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste versie staat bovenaan.
|
||||||
|
|
||||||
|
## 1.1.7 - 2026-11-09
|
||||||
|
|
||||||
|
### Gewijzigd
|
||||||
|
|
||||||
|
- Het icoon in het systeemvak is groter en sluit aan bij de andere systeemvakpictogrammen.
|
||||||
|
- De tekst bij het icoon is nu **kniip**. Tijdens een opname staat daar **kniip - opname loopt**.
|
||||||
|
|
||||||
|
## 1.1.6 - 2026-11-09
|
||||||
|
|
||||||
|
### Nieuw
|
||||||
|
|
||||||
|
- **Donkere modus.**\
|
||||||
|
Kies in de instellingen onder **Weergave** > **Kleurmodus** tussen **Licht**, **Donker** en **Systeem**. Bij **Systeem** volgt kniip de app-modus van Windows (Instellingen > Persoonlijke instellingen > Kleuren) en wisselt mee zodra je die verandert.
|
||||||
|
- De kleurmodus geldt voor alle vensters: de editor, de instellingen, de geschiedenis, de screencastvensters, de meldingen en het systeemvakmenu. Ook de titelbalk wordt donker.
|
||||||
|
- Na **Opslaan** wisselen open vensters meteen mee. Je knipsel zelf verandert niet van kleur.
|
||||||
|
- **Licht** is de standaard, dus na de update ziet kniip er hetzelfde uit tot je iets anders kiest.
|
||||||
|
|
||||||
|
### Gewijzigd
|
||||||
|
|
||||||
|
- De tab **Editor** in de instellingen heet nu **Weergave**. Daar staan nu de kleurmodus, de markeerstift en de werkbalkindeling.
|
||||||
|
- De tab **Windows** heet nu **Systeem** en staat onder **Weergave**. De tabs **Knipsels** en **Nieuw knipsel** zijn opgegaan in de tab **Systeem**. Bovenaan staat de sectie **Knipprogramma** (klembord en map Schermafbeeldingen bewaken), daaronder **Bij een nieuw knipsel** en daarna **Windows-integratie**. De instellingen openen nu op de tab **Sneltoetsen**.
|
||||||
|
- Vinkjes en keuzerondjes in de instellingen hebben een nieuwe, rustigere stijl in de kleur van kniip.
|
||||||
|
|
||||||
|
### Opgelost
|
||||||
|
|
||||||
|
- De tekst in een tab van de instellingen was helemaal blauw en vet, net als de naam van de tab. Nu is alleen de tabnaam zo opgemaakt. De changelog in de tab **Update** volgt weer de markdown: gewone tekst normaal, koppen en **vet** alleen waar dat in de tekst staat.
|
||||||
|
|
||||||
|
## 1.1.5 - 2026-11-09
|
||||||
|
|
||||||
|
### Nieuw
|
||||||
|
|
||||||
|
- **Split-weergave.**\
|
||||||
|
De editor kan nu ook twee werkbalken tonen. Bovenaan staan de gereedschappen in het midden, met **Instellingen** rechts en daaronder de eigenschappen (kleur, dikte, lijnstijl). Onderaan staan **Nieuw** en **Screencast** links, **Opslaan**, **Uploaden** en **Kopiëren** in het midden en de zoomknoppen rechts. Kies de indeling in de instellingen onder **Editor** > **Werkbalk**, of klik met rechts op een werkbalk en kies **Indeling**.
|
||||||
|
- In beide werkbalken kun je knoppen verslepen (na ontgrendelen) en verbergen, net als bij één werkbalk. Beide indelingen onthouden hun eigen volgorde.
|
||||||
|
- Split-weergave is de nieuwe standaard. Wil je de vertrouwde indeling terug, kies dan **Eén werkbalk**.
|
||||||
|
- **Vergrootglas bij het kiezen van een gebied.**\
|
||||||
|
Bij het knippen van een rechthoek en bij het kiezen van een opnamegebied voor een screencast loopt naast de muisaanwijzer een rond vergrootglas mee. Je ziet de pixels rond de aanwijzer vergroot met een raster en een blauwe kruisdraad, en daaronder de schermcoördinaten (X en Y). Zo zet je een hoek precies op de goede pixel. Bij **Venster knippen** verschijnt het vergrootglas niet.
|
||||||
|
- Het vergrootglas staat standaard aan. Wil je het niet, zet dan in de instellingen onder **Sneltoetsen** > **Gebied kiezen** het vinkje **Vergrootglas tonen bij het kiezen van een gebied** uit.
|
||||||
|
- **Schermafdruk vertraging.**\
|
||||||
|
In het systeemvakmenu staat **Schermafdruk vertraging: 0s**. Stel daar met de schuifbalk of het getalveld 0 tot 60 seconden in. **Rechthoek knippen** en **Venster knippen** wachten dan zo lang voordat het scherm bevriest, ook via de sneltoets. Zo kun je eerst een menu openklappen of een tooltip laten verschijnen.
|
||||||
|
- Bij de muis telt een klein venster af. Dat venster komt zelf niet in het knipsel. Druk op Esc of klik op het venster om te annuleren.
|
||||||
|
- Bij 0 seconden knip je direct, zoals voorheen. Screencasts en de knop **Nieuw** in de editor wachten niet.
|
||||||
|
- **Geschiedenis.**\
|
||||||
|
kniip houdt nu bij wat je opslaat en uploadt: knipsels en screencasts, met het bestand en de link. Knipsels die je alleen kopieert of niet bewaart, komen er niet in. Het systeemvakmenu heeft vier nieuwe items:
|
||||||
|
- **Map openen**, met de map Schermafbeeldingen, je knipselmap en je screencastmap.
|
||||||
|
- **Geschiedenis...** opent een lijst met datum, soort, naam, map en link. Je kunt zoeken op naam of link.
|
||||||
|
- **Afbeeldingsgeschiedenis...** opent hetzelfde venster met miniaturen. Ook een knipsel dat je alleen hebt geüpload heeft een miniatuur.
|
||||||
|
- **Recente items** toont de laatste tien. Klik op een item om het te openen, of ga naar het submenu voor **Openen in de editor**, **Link kopiëren**, **Afbeelding kopiëren**, **Pad kopiëren** en **Tonen in map**.
|
||||||
|
|
||||||
|
Dubbelklik in het geschiedenisvenster om een item te openen. Klik met rechts voor dezelfde acties en voor **Uit geschiedenis verwijderen**. Met **Geschiedenis wissen** maak je de lijst leeg. Je bestanden en uploads blijven dan gewoon bestaan. kniip bewaart de laatste 500 items.
|
||||||
|
|
||||||
|
## 1.1.4 - 2026-11-09
|
||||||
|
|
||||||
|
### Nieuw
|
||||||
|
|
||||||
|
- **Melding bij een nieuwe versie.**\
|
||||||
|
kniip kijkt bij het starten en daarna elke dag of er een nieuwere versie is. Is die er, dan staat er een blauwe stip op de knop **Instellingen** in de editor en op de tab **Update** in de instellingen. Wil je dat niet, zet dan in de tab **Update** het vinkje **Automatisch controleren op nieuwe versies** uit.
|
||||||
|
- **Uitgebreid rechtermuisknopmenu in de editor.**\
|
||||||
|
Klik met rechts op het canvas, ook op een leeg stuk, voor een menu met iconen en sneltoetsen. Wat op dat moment niet kan, staat grijs.
|
||||||
|
- **Ongedaan maken** en **Opnieuw**.
|
||||||
|
- **Verwijder**, **Verwijder alles** (Shift+Delete, de afbeelding blijft staan) en **Samenvoegen** (Ctrl+Shift+F). Samenvoegen maakt de annotaties deel van de afbeelding en kun je ongedaan maken.
|
||||||
|
- **Knip** (Ctrl+X), **Kopieer** (Ctrl+Shift+C), **Plak** (Ctrl+V) en **Dupliceren** (Ctrl+D). Plak via het menu zet de annotatie op de plek waar je rechts klikte. Kopiëren en plakken werkt ook tussen twee editorvensters.
|
||||||
|
- **Naar voorgrond** (Home), **Naar voren** (PageUp), **Naar achteren** (PageDown) en **Naar achtergrond** (End).
|
||||||
|
- **Werkbalken verbergen** (Ctrl+H). Via hetzelfde menu of Ctrl+H haal je ze terug. Bij een nieuwe start zijn ze er altijd weer.
|
||||||
|
- **Lijnen en pijlen buigen.**\
|
||||||
|
Selecteer een lijn of pijl. Midden op elk stuk staat dan een klein rondje. Sleep dat rondje en het wordt een buigpunt. De lijn loopt daarna als vloeiende kromme door alle punten. Zo kun je zoveel buigpunten toevoegen als je wilt.
|
||||||
|
- Dubbelklik op een buigpunt om het weer te verwijderen.
|
||||||
|
- Bij een pijl draait de punt mee met de richting van het laatste stuk.
|
||||||
|
- Met Shift springen begin- en eindpunt in stappen van 45°, gemeten vanaf het buigpunt ernaast.
|
||||||
|
- Toevoegen, verschuiven en verwijderen van buigpunten kun je ongedaan maken.
|
||||||
|
|
||||||
|
### Gewijzigd
|
||||||
|
|
||||||
|
- kniip zoekt nu standaard zelf op de achtergrond naar updates. Tot nu toe gebeurde dat alleen als je de tab **Update** opende of op **Opnieuw controleren** klikte. Een mislukte controle, bijvoorbeeld zonder internet, geeft geen foutmelding.
|
||||||
|
- **Ctrl+V** plakt nu eerst een gekopieerde annotatie. Heb je daarna iets anders gekopieerd, dan opent Ctrl+V zoals voorheen de afbeelding van het klembord.
|
||||||
|
- Het rechtermuisknopmenu van de werkbalk heeft dezelfde nieuwe stijl gekregen.
|
||||||
|
- **Testversie naast kniip.**\
|
||||||
|
`build.ps1` maakt nu ook `KniipTest-<versie>.exe`. Die testversie heeft eigen instellingen en registervermeldingen, zodat hij naast de geïnstalleerde kniip kan draaien zonder die te raken. De testversie werkt zichzelf niet bij. Met `-SkipTest` sla je hem over.
|
||||||
|
|
||||||
|
## 1.1.3 - 2026-11-09
|
||||||
|
|
||||||
|
### Nieuw
|
||||||
|
|
||||||
|
- **Sneltoets voor het klembord.**\
|
||||||
|
In de tab **Sneltoetsen** kun je een combinatie zetten op **Vanaf klembord**. Die opent de afbeelding op het klembord in de editor. Standaard is er geen sneltoets. De gekozen combinatie staat ook bij **Bewerken vanaf klembord** in het systeemvakmenu.
|
||||||
|
- **Afbeelding naar de editor slepen.**\
|
||||||
|
Sleep een afbeeldingsbestand, of een afbeelding uit een ander programma, naar het kniip-venster. kniip vraagt daarna of je die afbeelding wilt openen, zodat een lopende bewerking niet zomaar verdwijnt.
|
||||||
|
|
||||||
|
## 1.1.2 - 2026-11-09
|
||||||
|
|
||||||
|
### Opgelost
|
||||||
|
|
||||||
|
- **Link kopiëren na uploaden.**\
|
||||||
|
Na het uploaden kon kniip de link soms niet op het klembord zetten en meldde dan dat het klembord bezet was. Dat gebeurde vooral als de klembordgeschiedenis van Windows (Win+V) of een ander klembordprogramma actief is. kniip zet de link nu op een directere manier op het klembord en probeert het langer opnieuw. Dit geldt ook voor het uploaden van screencasts en de test-upload in de instellingen.
|
||||||
|
|
||||||
## 1.1.1 - 2026-11-09
|
## 1.1.1 - 2026-11-09
|
||||||
|
|
||||||
### Gewijzigd
|
### Gewijzigd
|
||||||
|
|
||||||
- **Instellingen zonder scrollbalk.** Het instellingenvenster is iets hoger geworden, zodat elke tab, ook **Opslaan** met de nieuwe naamvelden, in één keer past. De changelog in de tab **Update** heeft daardoor ook meer ruimte.
|
- **Instellingen zonder scrollbalk.**\
|
||||||
|
Het instellingenvenster is iets hoger geworden, zodat elke tab, ook **Opslaan** met de nieuwe naamvelden, in één keer past. De changelog in de tab **Update** heeft daardoor ook meer ruimte.
|
||||||
|
|
||||||
### Opgelost
|
### Opgelost
|
||||||
|
|
||||||
@@ -16,11 +112,13 @@ Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste vers
|
|||||||
|
|
||||||
### Nieuw
|
### Nieuw
|
||||||
|
|
||||||
- **Updaten vanuit kniip.** In de instellingen staat een nieuwe tab **Update**. kniip kijkt op git.hibbit.net of er een nieuwere versie is en toont de wijzigingen van alle nieuwere versies, netjes opgemaakt. Met **Updaten** downloadt kniip de installer, sluit zichzelf af, installeert de nieuwe versie op de achtergrond en start daarna opnieuw. Je eerder gekozen instellingen, zoals starten met Windows en 'Openen met', blijven staan.
|
- **Updaten vanuit kniip.**\
|
||||||
|
In de instellingen staat een nieuwe tab **Update**. kniip kijkt op git.hibbit.net of er een nieuwere versie is en toont de wijzigingen van alle nieuwere versies, netjes opgemaakt. Met **Updaten** downloadt kniip de installer, sluit zichzelf af, installeert de nieuwe versie op de achtergrond en start daarna opnieuw. Je eerder gekozen instellingen, zoals starten met Windows en 'Openen met', blijven staan.
|
||||||
- Na een update meldt kniip in het systeemvak naar welke versie het is bijgewerkt.
|
- Na een update meldt kniip in het systeemvak naar welke versie het is bijgewerkt.
|
||||||
- Er wordt alleen gecontroleerd als je de tab opent of op **Opnieuw controleren** klikt; kniip zoekt niet zelf op de achtergrond naar updates.
|
- Er wordt alleen gecontroleerd als je de tab opent of op **Opnieuw controleren** klikt; kniip zoekt niet zelf op de achtergrond naar updates.
|
||||||
- Loopt er nog een screencast, of wil je een open knipsel eerst opslaan, dan wacht de update tot dat klaar is.
|
- Loopt er nog een screencast, of wil je een open knipsel eerst opslaan, dan wacht de update tot dat klaar is.
|
||||||
- **Naamsjabloon voor opslaan.** In de tab **Opslaan** stel je per soort (knipsels en screencasts) zelf in hoe bestanden heten. Je kunt deze codes gebruiken:
|
- **Naamsjabloon voor opslaan.**\
|
||||||
|
In de tab **Opslaan** stel je per soort (knipsels en screencasts) zelf in hoe bestanden heten. Je kunt deze codes gebruiken:
|
||||||
- `%J` jaar, `%M` maand en `%D` dag (bijvoorbeeld `2026`, `11`, `09`)
|
- `%J` jaar, `%M` maand en `%D` dag (bijvoorbeeld `2026`, `11`, `09`)
|
||||||
- `%V` de naam van het programma waarvan je een knipsel of opname maakt, bijvoorbeeld `chrome`
|
- `%V` de naam van het programma waarvan je een knipsel of opname maakt, bijvoorbeeld `chrome`
|
||||||
- `%R6` zes willekeurige letters en cijfers
|
- `%R6` zes willekeurige letters en cijfers
|
||||||
@@ -28,7 +126,8 @@ Alle noemenswaardige wijzigingen in kniip staan in dit bestand. De nieuwste vers
|
|||||||
- `/` maakt een submap; kniip maakt die mappen zelf aan.
|
- `/` maakt een submap; kniip maakt die mappen zelf aan.
|
||||||
|
|
||||||
Zo wordt `%J/%M-%D/%V_%N3` bijvoorbeeld `2026/11-09/chrome_001.png`. Onder het veld zie je meteen een voorbeeld. De extensie (`.png`, `.mp4` of `.gif`) zet kniip er zelf achter. Laat je het veld leeg, dan blijft de vertrouwde naam staan: `Knipsel` of `Screencast` met datum en tijd.
|
Zo wordt `%J/%M-%D/%V_%N3` bijvoorbeeld `2026/11-09/chrome_001.png`. Onder het veld zie je meteen een voorbeeld. De extensie (`.png`, `.mp4` of `.gif`) zet kniip er zelf achter. Laat je het veld leeg, dan blijft de vertrouwde naam staan: `Knipsel` of `Screencast` met datum en tijd.
|
||||||
- **Autostart-vermelding opruimen.** Heb je kniip ooit als losse exe gestart, dan kan er een vermelding in **Instellingen > Apps > Opstarten** achterblijven. Met `tools\remove-autostart.cmd` haal je die weg. Beheerdersrechten zijn daarvoor niet nodig.
|
- **Autostart-vermelding opruimen.**\
|
||||||
|
Heb je kniip ooit als losse exe gestart, dan kan er een vermelding in **Instellingen > Apps > Opstarten** achterblijven. Met `tools\remove-autostart.cmd` haal je die weg. Beheerdersrechten zijn daarvoor niet nodig.
|
||||||
|
|
||||||
### Gewijzigd
|
### Gewijzigd
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
<h1 align="center">kniip</h1>
|
<h1 align="center">kniip</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Annotatie-editor naast het Windows Knipprogramma
|
<b>kniip</b> is een uitgebreide annotatie-editor voor Windows. Leg iets op je scherm vast, maak het duidelijk met annotaties en deel het als afbeelding, video, GIF of link.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://git.hibbit.net/rogier/kniip/releases"><strong>Download de installer</strong></a>
|
<a href="https://git.hibbit.net/rogier/kniip/releases"><strong>Download: kniip</strong></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -17,17 +17,26 @@
|
|||||||
kniip is een annotatie-editor die naast het Windows Knipprogramma werkt. Maak een schermafbeelding, voeg pijlen, tekst, markeringen, stappen, emoji of vervaging toe en sla het resultaat op, kopieer het of upload het. kniip kan ook schermopnames maken en die exporteren als video of GIF.
|
kniip is een annotatie-editor die naast het Windows Knipprogramma werkt. Maak een schermafbeelding, voeg pijlen, tekst, markeringen, stappen, emoji of vervaging toe en sla het resultaat op, kopieer het of upload het. kniip kan ook schermopnames maken en die exporteren als video of GIF.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="screenshot/knipp.png"><img src="screenshot/knipp.png" alt="Screenshot van de kniip-editor" width="720"></a>
|
<a href="screenshot/kniip.png"><img src="screenshot/kniip.png" alt="Screenshot van de kniip-editor" width="720"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## Wat kan kniip?
|
||||||
|
|
||||||
|
- **Knippen tot op de pixel.** Rechthoek of venster, met een vergrootglas voor precieze hoeken en een instelbare vertraging om eerst een menu of tooltip te openen.
|
||||||
|
- **Aantekenen.** Pijlen en lijnen die je kunt buigen, tekst, vormen, genummerde stappen, markeerstift, emoji en vervaging.
|
||||||
|
- **Screencasts.** Neem een deel van je scherm op en exporteer als video of GIF.
|
||||||
|
- **Delen.** Opslaan, kopiëren naar het klembord of uploaden en de link meteen delen.
|
||||||
|
- **Geschiedenis.** Vind eerdere knipsels en uploads terug, met miniaturen en zoeken.
|
||||||
|
- **Jouw werkbalk.** Kies een of twee werkbalken, versleep knoppen en verberg wat je niet gebruikt.
|
||||||
|
- **Altijd bij.** kniip meldt zelf wanneer er een nieuwe versie is en werkt zichzelf bij met een klik.
|
||||||
|
|
||||||
## Downloaden
|
## Downloaden
|
||||||
|
|
||||||
| Versie | Installer |
|
| Versie | Installer |
|
||||||
| ------ | --------- |
|
| ------ | --------- |
|
||||||
| Nieuwste versie | [Alle releases](https://git.hibbit.net/rogier/kniip/releases) |
|
| 1.1.7 | [Kniip-Setup-1.1.7.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.1.7/Kniip-Setup-1.1.7.exe) |
|
||||||
| 1.1.1 | [Kniip-Setup-1.1.1.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.1.1/Kniip-Setup-1.1.1.exe) |
|
|
||||||
| 1.1.0 | [Kniip-Setup-1.1.0.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.1.0/Kniip-Setup-1.1.0.exe) |
|
Oudere versies vind je bij [alle releases](https://git.hibbit.net/rogier/kniip/releases).
|
||||||
| 1.0.0 | [Kniip-Setup-1.0.0.exe](https://git.hibbit.net/rogier/kniip/releases/download/1.0/Kniip-Setup-1.0.0.exe) |
|
|
||||||
|
|
||||||
## Installeren
|
## Installeren
|
||||||
|
|
||||||
@@ -71,7 +80,7 @@ Vereisten:
|
|||||||
Bouwen:
|
Bouwen:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\build.ps1 # versie 1.1.1
|
.\build.ps1 # versie 1.1.7
|
||||||
.\build.ps1 -Version 1.2.0 # andere versie
|
.\build.ps1 -Version 1.2.0 # andere versie
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -80,18 +89,34 @@ Het script levert op:
|
|||||||
- `artifacts\publish\Kniip.exe`: zelfstandige exe, draait zonder installatie
|
- `artifacts\publish\Kniip.exe`: zelfstandige exe, draait zonder installatie
|
||||||
- `artifacts\Kniip-<versie>.exe`: dezelfde exe, met het versienummer in de naam
|
- `artifacts\Kniip-<versie>.exe`: dezelfde exe, met het versienummer in de naam
|
||||||
- `artifacts\Kniip-Setup-<versie>.exe`: de installer (alleen als Inno Setup 6 gevonden is)
|
- `artifacts\Kniip-Setup-<versie>.exe`: de installer (alleen als Inno Setup 6 gevonden is)
|
||||||
|
- `artifacts\KniipTest-<versie>.exe`: testversie (overslaan met `-SkipTest`)
|
||||||
|
|
||||||
|
### Testversie
|
||||||
|
|
||||||
|
De testversie draait naast de geïnstalleerde kniip en raakt die niet aan. Hij gebruikt overal `KniipTest` in plaats van `Kniip`: de autostartregel, de 'Openen met'-registratie (`KniipTest.Image`), de instellingenmap `%APPDATA%\KniipTest` en de single-instance-mutex. Bij de eerste start kopieert hij de instellingen van de gewone kniip. Zichzelf bijwerken doet hij niet, want de installer zou de gewone kniip overschrijven.
|
||||||
|
|
||||||
|
Opruimen hoeft alleen als je in de testversie autostart of 'Openen met' hebt aangezet:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\artifacts\KniipTest-<versie>.exe --unregister
|
||||||
|
Remove-Item "$env:APPDATA\KniipTest" -Recurse # optioneel: instellingen van de testversie
|
||||||
|
```
|
||||||
|
|
||||||
|
Draaien beide versies tegelijk, dan reageren ze allebei op dezelfde sneltoetsen en knipsels. Sluit de gewone kniip tijdens het testen, of kies in de testversie andere sneltoetsen.
|
||||||
|
|
||||||
## Een nieuwe versie publiceren
|
## Een nieuwe versie publiceren
|
||||||
|
|
||||||
1. Bouw met `.\build.ps1 -Version <versie>`.
|
1. Bouw met `.\build.ps1 -Version <versie>`.
|
||||||
2. Maak op [git.hibbit.net](https://git.hibbit.net/rogier/kniip/releases) een release met tag `v<versie>` en voeg `artifacts\Kniip-Setup-<versie>.exe` toe als bijlage.
|
2. Maak op [git.hibbit.net](https://git.hibbit.net/rogier/kniip/releases) een release met tag `v<versie>` en voeg `artifacts\Kniip-Setup-<versie>.exe` toe als bijlage.
|
||||||
3. Schrijf de wijzigingen in markdown in de beschrijving van de release. kniip toont die als changelog onder **Instellingen > Update**.
|
3. Schrijf de wijzigingen in markdown in de beschrijving van de release. kniip toont die als changelog onder **Instellingen > Update**.
|
||||||
4. Voeg de nieuwe versie toe aan de tabel onder [Downloaden](#downloaden).
|
4. Vervang de versie en de link in de tabel onder [Downloaden](#downloaden) door de nieuwe versie.
|
||||||
|
|
||||||
### Updaten vanuit kniip
|
### Updaten vanuit kniip
|
||||||
|
|
||||||
Onder **Instellingen > Update** controleert kniip via de API van git.hibbit.net of er een nieuwere release is en toont de changelog van alle nieuwere versies. **Updaten** downloadt de installer, sluit kniip, installeert stil en start kniip opnieuw. De eerder gekozen integraties blijven behouden.
|
Onder **Instellingen > Update** controleert kniip via de API van git.hibbit.net of er een nieuwere release is en toont de changelog van alle nieuwere versies. **Updaten** downloadt de installer, sluit kniip, installeert stil en start kniip opnieuw. De eerder gekozen integraties blijven behouden.
|
||||||
|
|
||||||
|
kniip controleert ook zelf bij het starten en daarna elke 24 uur. Is er een nieuwere versie, dan staat er een blauwe stip op de knop **Instellingen** in de editor en op de tab **Update**. Dit zet je uit met **Automatisch controleren op nieuwe versies** in die tab.
|
||||||
|
|
||||||
Voorwaarden:
|
Voorwaarden:
|
||||||
|
|
||||||
- De repository `rogier/kniip` is publiek; kniip gebruikt geen token.
|
- De repository `rogier/kniip` is publiek; kniip gebruikt geen token.
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
# Publiceert kniip als zelfstandige single-file exe (geen .NET-installatie nodig op de doel-pc)
|
# Publiceert kniip als zelfstandige single-file exe (geen .NET-installatie nodig op de doel-pc)
|
||||||
# en bouwt de installer als Inno Setup 6 aanwezig is.
|
# en bouwt de installer als Inno Setup 6 aanwezig is.
|
||||||
# ScreenRecorderLib (screencasts) is een C++/CLI-assembly die niet uit de bundel geladen kan worden;
|
# ScreenRecorderLib (screencasts) is een C++/CLI-assembly die niet uit de bundel geladen kan worden;
|
||||||
# IncludeAllContentForSelfExtract pakt daarom bij de eerste start alles uit naar %TEMP%\.net.
|
# IncludeAllContentForSelfExtract pakt daarom bij de eerste start alles uit naar %TEMP%\.net.
|
||||||
#
|
#
|
||||||
# .\build.ps1 -> artifacts\publish\Kniip.exe (+ installer indien mogelijk)
|
#
|
||||||
|
# Elke build levert ook artifacts\KniipTest-<versie>.exe: een testversie met eigen register-, instellingen-
|
||||||
|
# en mutexnamen (KniipTest), die naast de geïnstalleerde kniip draait zonder diens registratie te raken.
|
||||||
|
#
|
||||||
|
# .\build.ps1 -> artifacts\publish\Kniip.exe (+ installer indien mogelijk) + testexe
|
||||||
# .\build.ps1 -Version 1.2.0
|
# .\build.ps1 -Version 1.2.0
|
||||||
|
# .\build.ps1 -SkipTest -> zonder testexe
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[string]$Version = "1.1.1",
|
[string]$Version = "1.1.7",
|
||||||
[string]$Runtime = "win-x64"
|
[string]$Runtime = "win-x64",
|
||||||
|
[switch]$SkipTest
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$root = $PSScriptRoot
|
$root = $PSScriptRoot
|
||||||
|
$project = Join-Path $root "src\Kniip\Kniip.csproj"
|
||||||
$publishDir = Join-Path $root "artifacts\publish"
|
$publishDir = Join-Path $root "artifacts\publish"
|
||||||
|
$testPublishDir = Join-Path $root "artifacts\publish-test"
|
||||||
|
|
||||||
# Gebruik een SDK in het gebruikersprofiel als er geen systeembrede SDK is.
|
# Gebruik een SDK in het gebruikersprofiel als er geen systeembrede SDK is.
|
||||||
$localDotnet = Join-Path $env:LOCALAPPDATA "Microsoft\dotnet"
|
$localDotnet = Join-Path $env:LOCALAPPDATA "Microsoft\dotnet"
|
||||||
@@ -22,9 +30,10 @@ if (-not (dotnet --list-sdks 2>$null) -and (Test-Path (Join-Path $localDotnet "d
|
|||||||
$env:PATH = "$localDotnet;$env:PATH"
|
$env:PATH = "$localDotnet;$env:PATH"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $publishDir) { Remove-Item $publishDir -Recurse -Force }
|
function Publish-Kniip([string]$outputDir, [bool]$test) {
|
||||||
|
if (Test-Path $outputDir) { Remove-Item $outputDir -Recurse -Force }
|
||||||
|
|
||||||
dotnet publish (Join-Path $root "src\Kniip\Kniip.csproj") `
|
dotnet publish $project `
|
||||||
-c Release -r $Runtime --self-contained true `
|
-c Release -r $Runtime --self-contained true `
|
||||||
-p:PublishSingleFile=true `
|
-p:PublishSingleFile=true `
|
||||||
-p:IncludeNativeLibrariesForSelfExtract=true `
|
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||||
@@ -32,8 +41,12 @@ dotnet publish (Join-Path $root "src\Kniip\Kniip.csproj") `
|
|||||||
-p:EnableCompressionInSingleFile=true `
|
-p:EnableCompressionInSingleFile=true `
|
||||||
-p:DebugType=none `
|
-p:DebugType=none `
|
||||||
-p:Version=$Version `
|
-p:Version=$Version `
|
||||||
-o $publishDir
|
-p:KniipTest=$($test.ToString().ToLowerInvariant()) `
|
||||||
if ($LASTEXITCODE -ne 0) { throw "dotnet publish is mislukt." }
|
-o $outputDir
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "dotnet publish is mislukt." }
|
||||||
|
}
|
||||||
|
|
||||||
|
Publish-Kniip $publishDir $false
|
||||||
|
|
||||||
Write-Host "Gepubliceerd: $publishDir\Kniip.exe" -ForegroundColor Green
|
Write-Host "Gepubliceerd: $publishDir\Kniip.exe" -ForegroundColor Green
|
||||||
|
|
||||||
@@ -55,3 +68,10 @@ if ($iscc) {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Inno Setup 6 niet gevonden; installer overgeslagen (winget install JRSoftware.InnoSetup)." -ForegroundColor Yellow
|
Write-Host "Inno Setup 6 niet gevonden; installer overgeslagen (winget install JRSoftware.InnoSetup)." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (-not $SkipTest) {
|
||||||
|
Publish-Kniip $testPublishDir $true
|
||||||
|
$testExe = Join-Path $root "artifacts\KniipTest-$Version.exe"
|
||||||
|
Copy-Item (Join-Path $testPublishDir "Kniip.exe") $testExe -Force
|
||||||
|
Write-Host "Testversie: $testExe" -ForegroundColor Green
|
||||||
|
}
|
||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
#define AppName "kniip"
|
#define AppName "kniip"
|
||||||
#define AppExe "Kniip.exe"
|
#define AppExe "Kniip.exe"
|
||||||
#ifndef AppVersion
|
#ifndef AppVersion
|
||||||
#define AppVersion "1.1.1"
|
#define AppVersion "1.1.7"
|
||||||
#endif
|
#endif
|
||||||
#ifndef PublishDir
|
#ifndef PublishDir
|
||||||
#define PublishDir "..\artifacts\publish"
|
#define PublishDir "..\artifacts\publish"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
+209
-48
@@ -3,17 +3,11 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
ShutdownMode="OnExplicitShutdown">
|
ShutdownMode="OnExplicitShutdown">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<SolidColorBrush x:Key="AccentBrush" Color="#0067C0" />
|
<ResourceDictionary>
|
||||||
<SolidColorBrush x:Key="AccentHoverBrush" Color="#1975C5" />
|
<!-- Kleuren: ThemeManager wisselt deze tussen Themes/Light.xaml en Themes/Dark.xaml; gebruik ze via DynamicResource -->
|
||||||
<SolidColorBrush x:Key="AccentPressedBrush" Color="#3183CA" />
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<SolidColorBrush x:Key="WindowBrush" Color="#F3F3F3" />
|
<ResourceDictionary Source="Themes/Light.xaml" />
|
||||||
<SolidColorBrush x:Key="ToolbarBrush" Color="#F9F9F9" />
|
</ResourceDictionary.MergedDictionaries>
|
||||||
<SolidColorBrush x:Key="HoverBrush" Color="#0F000000" />
|
|
||||||
<SolidColorBrush x:Key="PressedBrush" Color="#18000000" />
|
|
||||||
<SolidColorBrush x:Key="CheckedBrush" Color="#FFFFFF" />
|
|
||||||
<SolidColorBrush x:Key="DividerBrush" Color="#E5E5E5" />
|
|
||||||
<SolidColorBrush x:Key="InkBrush" Color="#1A1A1A" />
|
|
||||||
<SolidColorBrush x:Key="SubtleTextBrush" Color="#5C5C5C" />
|
|
||||||
<FontFamily x:Key="IconFont">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
<FontFamily x:Key="IconFont">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||||
|
|
||||||
<Style TargetType="ToolTip">
|
<Style TargetType="ToolTip">
|
||||||
@@ -24,7 +18,7 @@
|
|||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="Transparent" />
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource InkBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource InkBrush}" />
|
||||||
<Setter Property="Padding" Value="8,5" />
|
<Setter Property="Padding" Value="8,5" />
|
||||||
<Setter Property="MinWidth" Value="36" />
|
<Setter Property="MinWidth" Value="36" />
|
||||||
<Setter Property="MinHeight" Value="34" />
|
<Setter Property="MinHeight" Value="34" />
|
||||||
@@ -42,10 +36,10 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource HoverBrush}" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource HoverBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsPressed" Value="True">
|
<Trigger Property="IsPressed" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource PressedBrush}" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource PressedBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
<Setter Property="Opacity" Value="0.35" />
|
<Setter Property="Opacity" Value="0.35" />
|
||||||
@@ -57,7 +51,7 @@
|
|||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="ProminentButton" TargetType="Button" BasedOn="{StaticResource FlatButton}">
|
<Style x:Key="ProminentButton" TargetType="Button" BasedOn="{StaticResource FlatButton}">
|
||||||
<Setter Property="Background" Value="#ECECEC" />
|
<Setter Property="Background" Value="{DynamicResource ProminentBrush}" />
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="Button">
|
<ControlTemplate TargetType="Button">
|
||||||
@@ -68,10 +62,10 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="#E0E0E0" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ProminentHoverBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsPressed" Value="True">
|
<Trigger Property="IsPressed" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="#D4D4D4" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ProminentPressedBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
<Setter Property="Opacity" Value="0.35" />
|
<Setter Property="Opacity" Value="0.35" />
|
||||||
@@ -93,20 +87,20 @@
|
|||||||
BorderThickness="1" CornerRadius="4" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
|
BorderThickness="1" CornerRadius="4" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</Border>
|
</Border>
|
||||||
<Border x:Name="Indicator" Height="3" Width="16" CornerRadius="1.5" Background="{StaticResource AccentBrush}"
|
<Border x:Name="Indicator" Height="3" Width="16" CornerRadius="1.5" Background="{DynamicResource AccentBrush}"
|
||||||
VerticalAlignment="Bottom" Margin="0,0,0,2" Visibility="Collapsed" />
|
VerticalAlignment="Bottom" Margin="0,0,0,2" Visibility="Collapsed" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource HoverBrush}" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource HoverBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsChecked" Value="True">
|
<Trigger Property="IsChecked" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource CheckedBrush}" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource CheckedBrush}" />
|
||||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#E0E0E0" />
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource ControlBorderBrush}" />
|
||||||
<Setter TargetName="Indicator" Property="Visibility" Value="Visible" />
|
<Setter TargetName="Indicator" Property="Visibility" Value="Visible" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsPressed" Value="True">
|
<Trigger Property="IsPressed" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource PressedBrush}" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource PressedBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
<Setter Property="Opacity" Value="0.35" />
|
<Setter Property="Opacity" Value="0.35" />
|
||||||
@@ -119,7 +113,7 @@
|
|||||||
|
|
||||||
<Style x:Key="AccentButton" TargetType="Button">
|
<Style x:Key="AccentButton" TargetType="Button">
|
||||||
<Setter Property="Foreground" Value="White" />
|
<Setter Property="Foreground" Value="White" />
|
||||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AccentBrush}" />
|
||||||
<Setter Property="Padding" Value="14,6" />
|
<Setter Property="Padding" Value="14,6" />
|
||||||
<Setter Property="MinHeight" Value="32" />
|
<Setter Property="MinHeight" Value="32" />
|
||||||
<Setter Property="FontFamily" Value="Segoe UI" />
|
<Setter Property="FontFamily" Value="Segoe UI" />
|
||||||
@@ -134,10 +128,10 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentHoverBrush}" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentHoverBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsPressed" Value="True">
|
<Trigger Property="IsPressed" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentPressedBrush}" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentPressedBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
<Setter Property="Opacity" Value="0.45" />
|
<Setter Property="Opacity" Value="0.45" />
|
||||||
@@ -149,8 +143,8 @@
|
|||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="SecondaryButton" TargetType="Button" BasedOn="{StaticResource FlatButton}">
|
<Style x:Key="SecondaryButton" TargetType="Button" BasedOn="{StaticResource FlatButton}">
|
||||||
<Setter Property="Background" Value="White" />
|
<Setter Property="Background" Value="{DynamicResource ControlBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="#D6D6D6" />
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrongBorderBrush}" />
|
||||||
<Setter Property="Padding" Value="12,5" />
|
<Setter Property="Padding" Value="12,5" />
|
||||||
<Setter Property="MinHeight" Value="32" />
|
<Setter Property="MinHeight" Value="32" />
|
||||||
<Setter Property="Margin" Value="4,0" />
|
<Setter Property="Margin" Value="4,0" />
|
||||||
@@ -164,7 +158,7 @@
|
|||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="IconPath" TargetType="Path">
|
<Style x:Key="IconPath" TargetType="Path">
|
||||||
<Setter Property="Stroke" Value="{StaticResource InkBrush}" />
|
<Setter Property="Stroke" Value="{DynamicResource InkBrush}" />
|
||||||
<Setter Property="StrokeThickness" Value="1" />
|
<Setter Property="StrokeThickness" Value="1" />
|
||||||
<Setter Property="StrokeLineJoin" Value="Round" />
|
<Setter Property="StrokeLineJoin" Value="Round" />
|
||||||
<Setter Property="StrokeStartLineCap" Value="Round" />
|
<Setter Property="StrokeStartLineCap" Value="Round" />
|
||||||
@@ -175,7 +169,7 @@
|
|||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="PropLabel" TargetType="TextBlock">
|
<Style x:Key="PropLabel" TargetType="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{StaticResource SubtleTextBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource SubtleTextBrush}" />
|
||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
<Setter Property="Margin" Value="0,0,6,0" />
|
<Setter Property="Margin" Value="0,0,6,0" />
|
||||||
@@ -199,10 +193,10 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsSelected" Value="True">
|
<Trigger Property="IsSelected" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="#1A0067C0" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SelectedBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsHighlighted" Value="True">
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource HoverBrush}" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource HoverBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</ControlTemplate.Triggers>
|
</ControlTemplate.Triggers>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
@@ -226,28 +220,30 @@
|
|||||||
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
|
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
|
||||||
<ToggleButton.Template>
|
<ToggleButton.Template>
|
||||||
<ControlTemplate TargetType="ToggleButton">
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
<Border x:Name="Bd" Background="White" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="4" />
|
<Border x:Name="Bd" Background="{DynamicResource ControlBrush}"
|
||||||
|
BorderBrush="{DynamicResource ControlBorderBrush}" BorderThickness="1" CornerRadius="4" />
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="#F5F5F5" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsChecked" Value="True">
|
<Trigger Property="IsChecked" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="#EBEBEB" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlPressedBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</ControlTemplate.Triggers>
|
</ControlTemplate.Triggers>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</ToggleButton.Template>
|
</ToggleButton.Template>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<StackPanel Orientation="Horizontal" IsHitTestVisible="False" Margin="8,0,6,0" VerticalAlignment="Center">
|
<DockPanel IsHitTestVisible="False" Margin="8,0,6,0" VerticalAlignment="Center">
|
||||||
|
<TextBlock DockPanel.Dock="Right" Text="" FontFamily="{StaticResource IconFont}" FontSize="8" Margin="6,1,0,0"
|
||||||
|
VerticalAlignment="Center" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
<ContentPresenter Content="{TemplateBinding SelectionBoxItem}"
|
<ContentPresenter Content="{TemplateBinding SelectionBoxItem}"
|
||||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<TextBlock Text="" FontFamily="{StaticResource IconFont}" FontSize="8" Margin="6,1,0,0"
|
</DockPanel>
|
||||||
VerticalAlignment="Center" Foreground="#5C5C5C" />
|
|
||||||
</StackPanel>
|
|
||||||
<Popup x:Name="PART_Popup" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}"
|
<Popup x:Name="PART_Popup" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}"
|
||||||
Placement="Bottom" AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
|
Placement="Bottom" AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
|
||||||
<Border Background="White" BorderBrush="#D6D6D6" BorderThickness="1" CornerRadius="6" Padding="3"
|
<Border Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource ControlStrongBorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6" Padding="3"
|
||||||
Margin="0,4,10,10" MinWidth="{TemplateBinding ActualWidth}">
|
Margin="0,4,10,10" MinWidth="{TemplateBinding ActualWidth}">
|
||||||
<Border.Effect>
|
<Border.Effect>
|
||||||
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.18" />
|
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.18" />
|
||||||
@@ -269,23 +265,23 @@
|
|||||||
<Setter Property="MinWidth" Value="30" />
|
<Setter Property="MinWidth" Value="30" />
|
||||||
<Setter Property="Margin" Value="2,0" />
|
<Setter Property="Margin" Value="2,0" />
|
||||||
<Setter Property="Padding" Value="6,0" />
|
<Setter Property="Padding" Value="6,0" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource InkBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource InkBrush}" />
|
||||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="ToggleButton">
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
<Border x:Name="Bd" Background="White" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="4"
|
<Border x:Name="Bd" Background="{DynamicResource ControlBrush}" BorderBrush="{DynamicResource ControlBorderBrush}"
|
||||||
Padding="{TemplateBinding Padding}">
|
BorderThickness="1" CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</Border>
|
</Border>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="#F5F5F5" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Trigger Property="IsChecked" Value="True">
|
<Trigger Property="IsChecked" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="#1A0067C0" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SelectedBrush}" />
|
||||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</ControlTemplate.Triggers>
|
</ControlTemplate.Triggers>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
@@ -293,11 +289,176 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style x:Key="PropCombo" TargetType="ComboBox">
|
<!-- Contextmenu's: icoon, tekst, sneltoets en eventueel een submenu -->
|
||||||
|
<Style TargetType="ContextMenu">
|
||||||
|
<Setter Property="FontFamily" Value="Segoe UI" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource InkBrush}" />
|
||||||
|
<Setter Property="HasDropShadow" Value="False" />
|
||||||
|
<Setter Property="Grid.IsSharedSizeScope" Value="True" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ContextMenu">
|
||||||
|
<Border Background="{DynamicResource ToolbarBrush}" BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1" CornerRadius="8"
|
||||||
|
Padding="4" Margin="0,0,10,10" MinWidth="220">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.18" />
|
||||||
|
</Border.Effect>
|
||||||
|
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle" />
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Separator">
|
||||||
|
<Border Height="1" Margin="6,4" Background="{DynamicResource DividerBrush}" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource InkBrush}" />
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="MenuItem">
|
||||||
|
<Border x:Name="Bd" Background="Transparent" CornerRadius="4" Padding="8,6" SnapsToDevicePixels="True">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="28" />
|
||||||
|
<ColumnDefinition Width="*" SharedSizeGroup="MenuHeader" />
|
||||||
|
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuGesture" />
|
||||||
|
<ColumnDefinition Width="16" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ContentPresenter x:Name="IconHost" ContentSource="Icon" VerticalAlignment="Center" HorizontalAlignment="Left" />
|
||||||
|
<TextBlock x:Name="Check" Text="" FontFamily="{StaticResource IconFont}" FontSize="12"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Left" Visibility="Collapsed" />
|
||||||
|
<ContentPresenter Grid.Column="1" ContentSource="Header" RecognizesAccessKey="True" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Grid.Column="2" Text="{TemplateBinding InputGestureText}" Margin="24,0,0,0"
|
||||||
|
Foreground="{DynamicResource SubtleTextBrush}" VerticalAlignment="Center" HorizontalAlignment="Right" />
|
||||||
|
<TextBlock x:Name="Arrow" Grid.Column="3" Text="" FontFamily="{StaticResource IconFont}" FontSize="10"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Collapsed" />
|
||||||
|
<Popup x:Name="PART_Popup" Placement="Right" HorizontalOffset="6" VerticalOffset="-6" AllowsTransparency="True"
|
||||||
|
Focusable="False" PopupAnimation="Fade" IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}">
|
||||||
|
<Border Background="{DynamicResource ToolbarBrush}" BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1" CornerRadius="8"
|
||||||
|
Padding="4" Margin="0,0,10,10" MinWidth="180" Grid.IsSharedSizeScope="True">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.18" />
|
||||||
|
</Border.Effect>
|
||||||
|
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle" />
|
||||||
|
</Border>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Check" Property="Visibility" Value="Visible" />
|
||||||
|
<Setter TargetName="IconHost" Property="Visibility" Value="Collapsed" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="HasItems" Value="True">
|
||||||
|
<Setter TargetName="Arrow" Property="Visibility" Value="Visible" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource HoverBrush}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="PropCombo" TargetType="ComboBox" BasedOn="{StaticResource MiniCombo}">
|
||||||
<Setter Property="MinWidth" Value="80" />
|
<Setter Property="MinWidth" Value="80" />
|
||||||
<Setter Property="Height" Value="28" />
|
<Setter Property="Height" Value="28" />
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
<Setter Property="Margin" Value="0" />
|
||||||
<Setter Property="FontSize" Value="12" />
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource InkBrush}" />
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="CheckBox">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border x:Name="Box" Width="16" Height="16" CornerRadius="3" BorderThickness="1" VerticalAlignment="Center"
|
||||||
|
Background="{DynamicResource ControlBrush}" BorderBrush="{DynamicResource CheckBoxBorderBrush}">
|
||||||
|
<TextBlock x:Name="Mark" Text="" FontFamily="{StaticResource IconFont}" FontSize="11" Foreground="White"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" />
|
||||||
|
</Border>
|
||||||
|
<ContentPresenter Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center" RecognizesAccessKey="True" />
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Box" Property="Background" Value="{DynamicResource ControlHoverBrush}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Box" Property="Background" Value="{DynamicResource AccentBrush}" />
|
||||||
|
<Setter TargetName="Box" Property="BorderBrush" Value="{DynamicResource AccentBrush}" />
|
||||||
|
<Setter TargetName="Mark" Property="Visibility" Value="Visible" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.45" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="RadioButton">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource InkBrush}" />
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid Width="16" Height="16" VerticalAlignment="Center">
|
||||||
|
<Ellipse x:Name="Ring" Fill="{DynamicResource ControlBrush}" Stroke="{DynamicResource CheckBoxBorderBrush}"
|
||||||
|
StrokeThickness="1" />
|
||||||
|
<Ellipse x:Name="Dot" Width="6" Height="6" Fill="White" Visibility="Collapsed" />
|
||||||
|
</Grid>
|
||||||
|
<ContentPresenter Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center" RecognizesAccessKey="True" />
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Ring" Property="Fill" Value="{DynamicResource ControlHoverBrush}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Ring" Property="Fill" Value="{DynamicResource AccentBrush}" />
|
||||||
|
<Setter TargetName="Ring" Property="Stroke" Value="{DynamicResource AccentBrush}" />
|
||||||
|
<Setter TargetName="Dot" Property="Visibility" Value="Visible" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.45" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="TextBox">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource InkBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrongBorderBrush}" />
|
||||||
|
<Setter Property="CaretBrush" Value="{DynamicResource InkBrush}" />
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
+106
-9
@@ -7,6 +7,7 @@ using System.Windows.Media.Imaging;
|
|||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using Kniip.Capture;
|
using Kniip.Capture;
|
||||||
using Kniip.Editor;
|
using Kniip.Editor;
|
||||||
|
using Kniip.History;
|
||||||
using Kniip.Infrastructure;
|
using Kniip.Infrastructure;
|
||||||
using Kniip.Interop;
|
using Kniip.Interop;
|
||||||
using Kniip.Notifications;
|
using Kniip.Notifications;
|
||||||
@@ -23,8 +24,10 @@ public partial class App : Application
|
|||||||
private SingleInstance? _singleInstance;
|
private SingleInstance? _singleInstance;
|
||||||
private TrayIcon? _tray;
|
private TrayIcon? _tray;
|
||||||
private SettingsWindow? _settingsWindow;
|
private SettingsWindow? _settingsWindow;
|
||||||
|
private HistoryWindow? _historyWindow;
|
||||||
private readonly List<RegionCaptureWindow> _snipWindows = new();
|
private readonly List<RegionCaptureWindow> _snipWindows = new();
|
||||||
private List<SnappedWindow> _snipSnapshot = new();
|
private List<SnappedWindow> _snipSnapshot = new();
|
||||||
|
private CountdownWindow? _snipCountdown;
|
||||||
private uint? _ignoredClipboardSequence;
|
private uint? _ignoredClipboardSequence;
|
||||||
|
|
||||||
public static App Instance => (App)Current;
|
public static App Instance => (App)Current;
|
||||||
@@ -38,6 +41,9 @@ public partial class App : Application
|
|||||||
/// <summary>Toetsenbordhaak voor alle sneltoetsen (knipsels en screencasts).</summary>
|
/// <summary>Toetsenbordhaak voor alle sneltoetsen (knipsels en screencasts).</summary>
|
||||||
internal GlobalHotkeyHook? HotkeyHook { get; private set; }
|
internal GlobalHotkeyHook? HotkeyHook { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Achtergrondcontrole op nieuwe versies; stuurt de blauwe stip bij Instellingen.</summary>
|
||||||
|
public UpdateNotifier Updates { get; } = new();
|
||||||
|
|
||||||
protected override void OnStartup(StartupEventArgs e)
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
@@ -71,6 +77,7 @@ public partial class App : Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
Settings = SettingsStore.Load();
|
Settings = SettingsStore.Load();
|
||||||
|
ThemeManager.Apply(Settings.ColorMode);
|
||||||
|
|
||||||
Capture = new CaptureCoordinator(Dispatcher);
|
Capture = new CaptureCoordinator(Dispatcher);
|
||||||
Capture.ApplySettings(Settings);
|
Capture.ApplySettings(Settings);
|
||||||
@@ -87,6 +94,12 @@ public partial class App : Application
|
|||||||
_tray.OpenFileRequested += OpenFileWithDialog;
|
_tray.OpenFileRequested += OpenFileWithDialog;
|
||||||
_tray.SnipRequested += BeginRegionSnip;
|
_tray.SnipRequested += BeginRegionSnip;
|
||||||
_tray.WindowSnipRequested += BeginWindowSnip;
|
_tray.WindowSnipRequested += BeginWindowSnip;
|
||||||
|
_tray.SetCaptureDelay(Settings.CaptureDelaySeconds);
|
||||||
|
_tray.CaptureDelayChanged += seconds =>
|
||||||
|
{
|
||||||
|
Settings.CaptureDelaySeconds = seconds;
|
||||||
|
SettingsStore.Save(Settings);
|
||||||
|
};
|
||||||
_tray.RecordRequested += () =>
|
_tray.RecordRequested += () =>
|
||||||
{
|
{
|
||||||
if (Recorder.Stage == RecordingStage.Idle)
|
if (Recorder.Stage == RecordingStage.Idle)
|
||||||
@@ -100,11 +113,16 @@ public partial class App : Application
|
|||||||
};
|
};
|
||||||
_tray.GifRequested += () => Recorder.Toggle(ScreencastFormat.Gif);
|
_tray.GifRequested += () => Recorder.Toggle(ScreencastFormat.Gif);
|
||||||
_tray.SettingsRequested += ShowSettings;
|
_tray.SettingsRequested += ShowSettings;
|
||||||
|
_tray.HistoryRequested += ShowHistory;
|
||||||
|
_tray.FoldersProvider = TrayFolders;
|
||||||
_tray.ExitRequested += ExitApplication;
|
_tray.ExitRequested += ExitApplication;
|
||||||
_tray.PauseChanged += _ => UpdateCapturePaused();
|
_tray.PauseChanged += _ => UpdateCapturePaused();
|
||||||
|
_tray.ApplyTheme(ThemeManager.IsDark);
|
||||||
|
ThemeManager.Changed += () => _tray?.ApplyTheme(ThemeManager.IsDark);
|
||||||
|
|
||||||
HotkeyHook = new GlobalHotkeyHook(Dispatcher);
|
HotkeyHook = new GlobalHotkeyHook(Dispatcher);
|
||||||
ApplyHotkeys();
|
ApplyHotkeys();
|
||||||
|
Updates.Apply(Settings.CheckForUpdates);
|
||||||
|
|
||||||
_singleInstance.StartServer(received => Dispatcher.BeginInvoke(() => HandleArguments(received, false)));
|
_singleInstance.StartServer(received => Dispatcher.BeginInvoke(() => HandleArguments(received, false)));
|
||||||
HandleArguments(args, true);
|
HandleArguments(args, true);
|
||||||
@@ -150,7 +168,7 @@ public partial class App : Application
|
|||||||
if (isFirstStart)
|
if (isFirstStart)
|
||||||
{
|
{
|
||||||
string? hotkey = HotkeyDisplay(Settings.SnipHotkey);
|
string? hotkey = HotkeyDisplay(Settings.SnipHotkey);
|
||||||
_tray?.ShowBalloon("kniip is actief", hotkey == null
|
_tray?.ShowBalloon($"{AppIdentity.DisplayName} is actief", hotkey == null
|
||||||
? "Maak een knipsel met Win + Shift + S. kniip biedt het daarna aan om te bewerken."
|
? "Maak een knipsel met Win + Shift + S. kniip biedt het daarna aan om te bewerken."
|
||||||
: $"Druk op {hotkey} en sleep een rechthoek om te knippen en te bewerken.");
|
: $"Druk op {hotkey} en sleep een rechthoek om te knippen en te bewerken.");
|
||||||
}
|
}
|
||||||
@@ -187,20 +205,59 @@ public partial class App : Application
|
|||||||
timer.Tick += (_, _) =>
|
timer.Tick += (_, _) =>
|
||||||
{
|
{
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
BeginRegionSnip();
|
BeginSnip(pickWindow: false);
|
||||||
};
|
};
|
||||||
timer.Start();
|
timer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Bevriest alle schermen en laat een rechthoek slepen; het resultaat opent in de editor.</summary>
|
/// <summary>Bevriest alle schermen (na de ingestelde vertraging) en laat een rechthoek slepen; het resultaat opent in de editor.</summary>
|
||||||
public void BeginRegionSnip() => BeginSnip(pickWindow: false);
|
public void BeginRegionSnip() => BeginDelayedSnip(pickWindow: false);
|
||||||
|
|
||||||
/// <summary>Bevriest alle schermen en laat een venster aanklikken; het resultaat opent in de editor.</summary>
|
/// <summary>Bevriest alle schermen (na de ingestelde vertraging) en laat een venster aanklikken; het resultaat opent in de editor.</summary>
|
||||||
public void BeginWindowSnip() => BeginSnip(pickWindow: true);
|
public void BeginWindowSnip() => BeginDelayedSnip(pickWindow: true);
|
||||||
|
|
||||||
|
private void BeginDelayedSnip(bool pickWindow)
|
||||||
|
{
|
||||||
|
if (_snipCountdown != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int seconds = Settings.CaptureDelaySeconds;
|
||||||
|
if (seconds <= 0 || _snipWindows.Count > 0)
|
||||||
|
{
|
||||||
|
BeginSnip(pickWindow);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var countdown = new CountdownWindow(seconds);
|
||||||
|
countdown.Elapsed += () =>
|
||||||
|
{
|
||||||
|
EndCountdown();
|
||||||
|
// Even wachten tot het aftelvenster echt van het scherm is.
|
||||||
|
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(150) };
|
||||||
|
timer.Tick += (_, _) =>
|
||||||
|
{
|
||||||
|
timer.Stop();
|
||||||
|
BeginSnip(pickWindow);
|
||||||
|
};
|
||||||
|
timer.Start();
|
||||||
|
};
|
||||||
|
countdown.Cancelled += EndCountdown;
|
||||||
|
_snipCountdown = countdown;
|
||||||
|
ApplyHotkeys();
|
||||||
|
countdown.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndCountdown()
|
||||||
|
{
|
||||||
|
_snipCountdown = null;
|
||||||
|
ApplyHotkeys();
|
||||||
|
}
|
||||||
|
|
||||||
private void BeginSnip(bool pickWindow)
|
private void BeginSnip(bool pickWindow)
|
||||||
{
|
{
|
||||||
if (_snipWindows.Count > 0 || Recorder.Stage == RecordingStage.Selecting)
|
if (_snipWindows.Count > 0 || _snipCountdown != null || Recorder.Stage == RecordingStage.Selecting)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -214,7 +271,7 @@ public partial class App : Application
|
|||||||
var frozen = Screen.AllScreens.Select(screen => (screen, ScreenGrabber.Capture(screen.Bounds))).ToList();
|
var frozen = Screen.AllScreens.Select(screen => (screen, ScreenGrabber.Capture(screen.Bounds))).ToList();
|
||||||
foreach ((Screen screen, BitmapSource image) in frozen)
|
foreach ((Screen screen, BitmapSource image) in frozen)
|
||||||
{
|
{
|
||||||
var window = new RegionCaptureWindow(screen, image, windows);
|
var window = new RegionCaptureWindow(screen, image, windows, Settings.ShowMagnifier);
|
||||||
window.Finished += OnRegionSnipFinished;
|
window.Finished += OnRegionSnipFinished;
|
||||||
_snipWindows.Add(window);
|
_snipWindows.Add(window);
|
||||||
window.Show();
|
window.Show();
|
||||||
@@ -298,11 +355,18 @@ public partial class App : Application
|
|||||||
Bind(Settings.WindowHotkey, BeginWindowSnip);
|
Bind(Settings.WindowHotkey, BeginWindowSnip);
|
||||||
Bind(Settings.RecordHotkey, () => Recorder.Toggle(ScreencastFormat.Mp4));
|
Bind(Settings.RecordHotkey, () => Recorder.Toggle(ScreencastFormat.Mp4));
|
||||||
Bind(Settings.GifHotkey, () => Recorder.Toggle(ScreencastFormat.Gif));
|
Bind(Settings.GifHotkey, () => Recorder.Toggle(ScreencastFormat.Gif));
|
||||||
|
Bind(Settings.ClipboardHotkey, OpenFromClipboard);
|
||||||
|
if (_snipCountdown is { } countdown)
|
||||||
|
{
|
||||||
|
// Alleen tijdens het aftellen; daarna moet Esc gewoon naar het actieve programma.
|
||||||
|
bindings[new HotkeyGesture(System.Windows.Input.ModifierKeys.None, System.Windows.Input.Key.Escape)] = countdown.Cancel;
|
||||||
|
}
|
||||||
|
|
||||||
HotkeyHook.Bindings = bindings;
|
HotkeyHook.Bindings = bindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
_tray?.SetHotkeys(HotkeyDisplay(Settings.SnipHotkey), HotkeyDisplay(Settings.WindowHotkey),
|
_tray?.SetHotkeys(HotkeyDisplay(Settings.SnipHotkey), HotkeyDisplay(Settings.WindowHotkey),
|
||||||
HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey));
|
HotkeyDisplay(Settings.RecordHotkey), HotkeyDisplay(Settings.GifHotkey), HotkeyDisplay(Settings.ClipboardHotkey));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk.</summary>
|
/// <summary>Start een screencast: eerst het gebied kiezen, daarna de knoppenbalk.</summary>
|
||||||
@@ -432,13 +496,46 @@ public partial class App : Application
|
|||||||
BringToFront(_settingsWindow);
|
BringToFront(_settingsWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ShowHistory(bool images)
|
||||||
|
{
|
||||||
|
if (_historyWindow == null)
|
||||||
|
{
|
||||||
|
_historyWindow = new HistoryWindow(images);
|
||||||
|
_historyWindow.Closed += (_, _) => _historyWindow = null;
|
||||||
|
_historyWindow.Show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_historyWindow.ShowImages(images);
|
||||||
|
}
|
||||||
|
|
||||||
|
BringToFront(_historyWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<(string Label, string? Path)> TrayFolders()
|
||||||
|
{
|
||||||
|
static string? Existing(params string?[] folders) =>
|
||||||
|
folders.FirstOrDefault(folder => folder is { Length: > 0 } && Directory.Exists(folder));
|
||||||
|
|
||||||
|
return new (string, string?)[]
|
||||||
|
{
|
||||||
|
("Schermafbeeldingen", Existing(CaptureCoordinator.ScreenshotsFolder)),
|
||||||
|
("Knipsels", Existing(Settings.SnipSaveFolder, Settings.LastSaveFolder,
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures))),
|
||||||
|
("Screencasts", Existing(Settings.VideoSaveFolder, Settings.LastVideoFolder,
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), "kniip")))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public void ShowBalloon(string title, string text) => _tray?.ShowBalloon(title, text);
|
public void ShowBalloon(string title, string text) => _tray?.ShowBalloon(title, text);
|
||||||
|
|
||||||
public void SaveSettings()
|
public void SaveSettings()
|
||||||
{
|
{
|
||||||
SettingsStore.Save(Settings);
|
SettingsStore.Save(Settings);
|
||||||
|
ThemeManager.Apply(Settings.ColorMode);
|
||||||
Capture.ApplySettings(Settings);
|
Capture.ApplySettings(Settings);
|
||||||
ApplyHotkeys();
|
ApplyHotkeys();
|
||||||
|
Updates.Apply(Settings.CheckForUpdates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void BringToFront(Window window)
|
private static void BringToFront(Window window)
|
||||||
|
|||||||
@@ -41,6 +41,58 @@ internal static class ClipboardImageReader
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ReadData(data, includeFiles) is { } image)
|
||||||
|
{
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Clipboard.ContainsImage() && Clipboard.GetImage() is { } bitmap)
|
||||||
|
{
|
||||||
|
return OpaqueCopy(bitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is COMException or ExternalException or NotSupportedException or FileFormatException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Snelle controle tijdens het slepen, zonder te decoderen.</summary>
|
||||||
|
public static bool CanDrop(IDataObject data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return data.GetDataPresent("PNG") || data.GetDataPresent(DataFormats.Dib) || data.GetDataPresent(DataFormats.Bitmap)
|
||||||
|
|| (data.GetData(DataFormats.FileDrop) is string[] files && Array.Exists(files, IsImageFile));
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is COMException or ExternalException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Afbeelding uit iets dat in kniip is neergezet: een afbeeldingsbestand of afbeeldingsdata.</summary>
|
||||||
|
public static BitmapSource? FromDrop(IDataObject data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ReadData(data, includeFiles: true) is { } image)
|
||||||
|
{
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.GetData(DataFormats.Bitmap) is BitmapSource bitmap ? OpaqueCopy(bitmap) : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is COMException or ExternalException or NotSupportedException or FileFormatException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BitmapSource? ReadData(IDataObject data, bool includeFiles)
|
||||||
|
{
|
||||||
if (includeFiles && TryReadFile(data) is { } fromFile)
|
if (includeFiles && TryReadFile(data) is { } fromFile)
|
||||||
{
|
{
|
||||||
return fromFile;
|
return fromFile;
|
||||||
@@ -53,31 +105,18 @@ internal static class ClipboardImageReader
|
|||||||
|
|
||||||
if (data.GetDataPresent(DataFormats.Dib) && data.GetData(DataFormats.Dib) is MemoryStream dib)
|
if (data.GetDataPresent(DataFormats.Dib) && data.GetData(DataFormats.Dib) is MemoryStream dib)
|
||||||
{
|
{
|
||||||
BitmapSource? fromDib = FromDib(dib.ToArray());
|
return FromDib(dib.ToArray());
|
||||||
if (fromDib != null)
|
|
||||||
{
|
|
||||||
return fromDib;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Clipboard.ContainsImage())
|
|
||||||
{
|
|
||||||
BitmapSource? image = Clipboard.GetImage();
|
|
||||||
if (image != null)
|
|
||||||
{
|
|
||||||
var copy = PixelUtil.CreateBitmap(PixelUtil.ToOpaqueBgra(image), image.PixelWidth, image.PixelHeight);
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is COMException or ExternalException or NotSupportedException or FileFormatException)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static BitmapSource OpaqueCopy(BitmapSource image) =>
|
||||||
|
PixelUtil.CreateBitmap(PixelUtil.ToOpaqueBgra(image), image.PixelWidth, image.PixelHeight);
|
||||||
|
|
||||||
|
private static bool IsImageFile(string file) =>
|
||||||
|
Array.IndexOf(ImageExtensions, Path.GetExtension(file).ToLowerInvariant()) >= 0;
|
||||||
|
|
||||||
private static BitmapSource? TryReadFile(IDataObject data)
|
private static BitmapSource? TryReadFile(IDataObject data)
|
||||||
{
|
{
|
||||||
if (!data.GetDataPresent(DataFormats.FileDrop) || data.GetData(DataFormats.FileDrop) is not string[] files)
|
if (!data.GetDataPresent(DataFormats.FileDrop) || data.GetData(DataFormats.FileDrop) is not string[] files)
|
||||||
@@ -87,7 +126,7 @@ internal static class ClipboardImageReader
|
|||||||
|
|
||||||
foreach (string file in files)
|
foreach (string file in files)
|
||||||
{
|
{
|
||||||
if (!File.Exists(file) || Array.IndexOf(ImageExtensions, Path.GetExtension(file).ToLowerInvariant()) < 0)
|
if (!File.Exists(file) || !IsImageFile(file))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Window x:Class="Kniip.Capture.CountdownWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="kniip" WindowStyle="None" AllowsTransparency="True" Background="Transparent"
|
||||||
|
Topmost="True" ShowInTaskbar="False" ShowActivated="False" ResizeMode="NoResize"
|
||||||
|
SizeToContent="WidthAndHeight" FontFamily="Segoe UI" Foreground="{DynamicResource InkBrush}" UseLayoutRounding="True">
|
||||||
|
<Border Margin="10" Background="{DynamicResource ToastBrush}" CornerRadius="8" BorderBrush="{DynamicResource ToastBorderBrush}" BorderThickness="1"
|
||||||
|
Cursor="Hand" MouseLeftButtonUp="Border_Click" ToolTip="Klik om te annuleren">
|
||||||
|
<Border.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="14" ShadowDepth="2" Opacity="0.22" />
|
||||||
|
</Border.Effect>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="12,8,14,8">
|
||||||
|
<TextBlock x:Name="SecondsText" FontSize="28" FontWeight="SemiBold" Foreground="{DynamicResource AccentTextBrush}"
|
||||||
|
MinWidth="34" TextAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
<StackPanel Margin="10,0,0,0" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Knipsel over" FontSize="12" FontWeight="SemiBold" />
|
||||||
|
<TextBlock Text="Esc om te annuleren" FontSize="11" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using Kniip.Interop;
|
||||||
|
using Kniip.Recording;
|
||||||
|
using Rectangle = System.Drawing.Rectangle;
|
||||||
|
using Screen = System.Windows.Forms.Screen;
|
||||||
|
|
||||||
|
namespace Kniip.Capture;
|
||||||
|
|
||||||
|
/// <summary>Klein venster bij de muis dat aftelt tot een vertraagd knipsel; neemt nooit de focus over.</summary>
|
||||||
|
public partial class CountdownWindow : Window
|
||||||
|
{
|
||||||
|
private const int CursorOffset = 20;
|
||||||
|
|
||||||
|
private readonly DispatcherTimer _follow = new() { Interval = TimeSpan.FromMilliseconds(40) };
|
||||||
|
private readonly DateTime _end;
|
||||||
|
private bool _finished;
|
||||||
|
|
||||||
|
/// <summary>De tijd is om; het venster is dan al gesloten.</summary>
|
||||||
|
public event Action? Elapsed;
|
||||||
|
|
||||||
|
/// <summary>Geannuleerd met Esc of een klik; het venster is dan al gesloten.</summary>
|
||||||
|
public event Action? Cancelled;
|
||||||
|
|
||||||
|
public CountdownWindow(int seconds)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Left = Top = -32000;
|
||||||
|
_end = DateTime.UtcNow.AddSeconds(seconds);
|
||||||
|
SecondsText.Text = seconds.ToString();
|
||||||
|
_follow.Tick += (_, _) => Tick();
|
||||||
|
SourceInitialized += (_, _) =>
|
||||||
|
{
|
||||||
|
NativeMethods.AddExtendedStyle(PhysicalWindow.Handle(this),
|
||||||
|
NativeMethods.WS_EX_TOOLWINDOW | NativeMethods.WS_EX_NOACTIVATE);
|
||||||
|
PhysicalWindow.ExcludeFromCapture(this);
|
||||||
|
};
|
||||||
|
ContentRendered += (_, _) => FollowCursor();
|
||||||
|
Closed += (_, _) =>
|
||||||
|
{
|
||||||
|
_follow.Stop();
|
||||||
|
if (!_finished)
|
||||||
|
{
|
||||||
|
_finished = true;
|
||||||
|
Cancelled?.Invoke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
Show();
|
||||||
|
FollowCursor();
|
||||||
|
_follow.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel() => Finish(Cancelled);
|
||||||
|
|
||||||
|
private void Tick()
|
||||||
|
{
|
||||||
|
TimeSpan left = _end - DateTime.UtcNow;
|
||||||
|
if (left <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
Finish(Elapsed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SecondsText.Text = ((int)Math.Ceiling(left.TotalSeconds)).ToString();
|
||||||
|
FollowCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FollowCursor()
|
||||||
|
{
|
||||||
|
if (!IsVisible)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeMethods.GetCursorPos(out NativeMethods.POINT cursor);
|
||||||
|
Rectangle area = Screen.FromPoint(new System.Drawing.Point(cursor.X, cursor.Y)).WorkingArea;
|
||||||
|
Rectangle size = PhysicalWindow.GetBounds(this);
|
||||||
|
int x = cursor.X + CursorOffset, y = cursor.Y + CursorOffset;
|
||||||
|
if (x + size.Width > area.Right)
|
||||||
|
{
|
||||||
|
x = cursor.X - CursorOffset - size.Width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y + size.Height > area.Bottom)
|
||||||
|
{
|
||||||
|
y = cursor.Y - CursorOffset - size.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Math.Max(area.Left, x);
|
||||||
|
y = Math.Max(area.Top, y);
|
||||||
|
if (x != size.X || y != size.Y)
|
||||||
|
{
|
||||||
|
PhysicalWindow.Place(this, new Rectangle(x, y, size.Width, size.Height), resize: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Finish(Action? handler)
|
||||||
|
{
|
||||||
|
if (_finished)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_finished = true;
|
||||||
|
Close();
|
||||||
|
handler?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Border_Click(object sender, MouseButtonEventArgs e) => Cancel();
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<UserControl x:Class="Kniip.Capture.MagnifierControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Top" IsHitTestVisible="False">
|
||||||
|
<StackPanel>
|
||||||
|
<Grid Width="120" Height="120">
|
||||||
|
<Grid.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.35" />
|
||||||
|
</Grid.Effect>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Clip>
|
||||||
|
<EllipseGeometry Center="60,60" RadiusX="60" RadiusY="60" />
|
||||||
|
</Grid.Clip>
|
||||||
|
<Image x:Name="Zoom" Stretch="Fill" RenderOptions.BitmapScalingMode="NearestNeighbor" />
|
||||||
|
<Rectangle>
|
||||||
|
<Rectangle.Fill>
|
||||||
|
<DrawingBrush Viewport="0,0,8,8" ViewportUnits="Absolute" TileMode="Tile">
|
||||||
|
<DrawingBrush.Drawing>
|
||||||
|
<GeometryDrawing>
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="#40000000" Thickness="0.5" />
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
<GeometryDrawing.Geometry>
|
||||||
|
<RectangleGeometry Rect="0,0,8,8" />
|
||||||
|
</GeometryDrawing.Geometry>
|
||||||
|
</GeometryDrawing>
|
||||||
|
</DrawingBrush.Drawing>
|
||||||
|
</DrawingBrush>
|
||||||
|
</Rectangle.Fill>
|
||||||
|
</Rectangle>
|
||||||
|
<Rectangle Height="8" VerticalAlignment="Top" Margin="0,56,0,0" Fill="{DynamicResource AccentBrush}" Opacity="0.55" />
|
||||||
|
<Rectangle Width="8" HorizontalAlignment="Left" Margin="56,0,0,0" Fill="{DynamicResource AccentBrush}" Opacity="0.55" />
|
||||||
|
<Rectangle Width="10" Height="10" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="55,55,0,0"
|
||||||
|
Stroke="White" StrokeThickness="1" />
|
||||||
|
<Rectangle Width="8" Height="8" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="56,56,0,0"
|
||||||
|
Stroke="#202020" StrokeThickness="1" />
|
||||||
|
</Grid>
|
||||||
|
<Ellipse Stroke="White" StrokeThickness="2" />
|
||||||
|
</Grid>
|
||||||
|
<Border HorizontalAlignment="Center" Margin="0,6,0,0" Background="#E6202020" CornerRadius="4" Padding="6,2">
|
||||||
|
<TextBlock x:Name="CoordinateText" Foreground="White" FontSize="12" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
|
namespace Kniip.Capture;
|
||||||
|
|
||||||
|
/// <summary>Rond vergrootglas met kruisdraad en coördinaten dat naast de muisaanwijzer meeloopt.</summary>
|
||||||
|
public partial class MagnifierControl : UserControl
|
||||||
|
{
|
||||||
|
private const int Pixels = 15;
|
||||||
|
private const double CursorOffset = 20;
|
||||||
|
|
||||||
|
private static MagnifierControl? _visible;
|
||||||
|
|
||||||
|
private readonly WriteableBitmap _zoom = new(Pixels, Pixels, 96, 96, PixelFormats.Bgra32, null);
|
||||||
|
private readonly byte[] _buffer = new byte[Pixels * Pixels * 4];
|
||||||
|
private readonly TranslateTransform _offset = new();
|
||||||
|
|
||||||
|
public MagnifierControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Zoom.Source = _zoom;
|
||||||
|
RenderTransform = _offset;
|
||||||
|
Visibility = Visibility.Collapsed;
|
||||||
|
Unloaded += (_, _) => Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <param name="frozen">Vastgelegd beeld van het scherm.</param>
|
||||||
|
/// <param name="relative">Muispositie in pixels van <paramref name="frozen"/>.</param>
|
||||||
|
/// <param name="screen">Muispositie in fysieke bureaubladcoördinaten, voor de tekst.</param>
|
||||||
|
/// <param name="cursor">Muispositie in het venster (DIP).</param>
|
||||||
|
/// <param name="area">Grootte van het venster (DIP).</param>
|
||||||
|
public void Show(BitmapSource frozen, System.Drawing.Point relative, System.Drawing.Point screen, Point cursor, Size area)
|
||||||
|
{
|
||||||
|
// Elk scherm heeft een eigen overlay; een overlay krijgt soms muisbewegingen terwijl de aanwijzer op een ander scherm staat.
|
||||||
|
if (relative.X < 0 || relative.Y < 0 || relative.X >= frozen.PixelWidth || relative.Y >= frozen.PixelHeight)
|
||||||
|
{
|
||||||
|
Hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_visible != this)
|
||||||
|
{
|
||||||
|
_visible?.Hide();
|
||||||
|
_visible = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyPixels(frozen, relative);
|
||||||
|
CoordinateText.Text = $"X: {screen.X} Y: {screen.Y}";
|
||||||
|
Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||||
|
Size size = DesiredSize;
|
||||||
|
double left = cursor.X + CursorOffset;
|
||||||
|
if (left + size.Width > area.Width)
|
||||||
|
{
|
||||||
|
left = cursor.X - CursorOffset - size.Width;
|
||||||
|
}
|
||||||
|
|
||||||
|
double top = cursor.Y + CursorOffset;
|
||||||
|
if (top + size.Height > area.Height)
|
||||||
|
{
|
||||||
|
top = cursor.Y - CursorOffset - size.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
_offset.X = Math.Max(0, left);
|
||||||
|
_offset.Y = Math.Max(0, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Hide()
|
||||||
|
{
|
||||||
|
Visibility = Visibility.Collapsed;
|
||||||
|
if (_visible == this)
|
||||||
|
{
|
||||||
|
_visible = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyPixels(BitmapSource frozen, System.Drawing.Point center)
|
||||||
|
{
|
||||||
|
Array.Clear(_buffer);
|
||||||
|
for (int i = 3; i < _buffer.Length; i += 4)
|
||||||
|
{
|
||||||
|
_buffer[i] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
int half = Pixels / 2;
|
||||||
|
int x0 = Math.Max(0, center.X - half);
|
||||||
|
int y0 = Math.Max(0, center.Y - half);
|
||||||
|
int x1 = Math.Min(frozen.PixelWidth, center.X + half + 1);
|
||||||
|
int y1 = Math.Min(frozen.PixelHeight, center.Y + half + 1);
|
||||||
|
if (x1 > x0 && y1 > y0)
|
||||||
|
{
|
||||||
|
int offset = ((y0 - (center.Y - half)) * Pixels + (x0 - (center.X - half))) * 4;
|
||||||
|
frozen.CopyPixels(new Int32Rect(x0, y0, x1 - x0, y1 - y0), _buffer, Pixels * 4, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
_zoom.WritePixels(new Int32Rect(0, 0, Pixels, Pixels), _buffer, Pixels * 4, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
<Window x:Class="Kniip.Capture.RegionCaptureWindow"
|
<Window x:Class="Kniip.Capture.RegionCaptureWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:Kniip.Capture"
|
||||||
Title="kniip rechthoek knippen" WindowStyle="None" ResizeMode="NoResize"
|
Title="kniip rechthoek knippen" WindowStyle="None" ResizeMode="NoResize"
|
||||||
ShowInTaskbar="False" Topmost="True" Cursor="Cross" FontFamily="Segoe UI" FontSize="13"
|
ShowInTaskbar="False" Topmost="True" Cursor="Cross" FontFamily="Segoe UI" FontSize="13"
|
||||||
Background="Black">
|
Background="Black">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Image x:Name="Frozen" Stretch="Fill" RenderOptions.BitmapScalingMode="NearestNeighbor" />
|
<Image x:Name="Frozen" Stretch="Fill" RenderOptions.BitmapScalingMode="NearestNeighbor" />
|
||||||
<Path x:Name="Dim" Fill="#73000000" />
|
<Path x:Name="Dim" Fill="#73000000" />
|
||||||
<Rectangle x:Name="SelectionRect" Stroke="{StaticResource AccentBrush}" StrokeThickness="2"
|
<Rectangle x:Name="SelectionRect" Stroke="{DynamicResource AccentBrush}" StrokeThickness="2"
|
||||||
HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False" />
|
HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False" />
|
||||||
<Border x:Name="SizeBadge" HorizontalAlignment="Left" VerticalAlignment="Top" Background="#E6202020"
|
<Border x:Name="SizeBadge" HorizontalAlignment="Left" VerticalAlignment="Top" Background="#E6202020"
|
||||||
CornerRadius="4" Padding="6,2" Visibility="Collapsed" IsHitTestVisible="False">
|
CornerRadius="4" Padding="6,2" Visibility="Collapsed" IsHitTestVisible="False">
|
||||||
<TextBlock x:Name="SizeText" Foreground="White" FontSize="12" />
|
<TextBlock x:Name="SizeText" Foreground="White" FontSize="12" />
|
||||||
</Border>
|
</Border>
|
||||||
<Border x:Name="Hint" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,28,0,0" Background="#F2FFFFFF"
|
<local:MagnifierControl x:Name="Magnifier" />
|
||||||
|
<Border x:Name="Hint" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,28,0,0" Background="{DynamicResource ToastBrush}"
|
||||||
CornerRadius="6" Padding="16,9" IsHitTestVisible="False">
|
CornerRadius="6" Padding="16,9" IsHitTestVisible="False">
|
||||||
<Border.Effect>
|
<Border.Effect>
|
||||||
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.25" />
|
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.25" />
|
||||||
</Border.Effect>
|
</Border.Effect>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock x:Name="HintTitle" Text="Knipsel: sleep een rechthoek" FontWeight="SemiBold" Foreground="{StaticResource InkBrush}" />
|
<TextBlock x:Name="HintTitle" Text="Knipsel: sleep een rechthoek" FontWeight="SemiBold" Foreground="{DynamicResource InkBrush}" />
|
||||||
<TextBlock Foreground="{StaticResource SubtleTextBrush}" FontSize="12" Margin="0,2,0,0"
|
<TextBlock Foreground="{DynamicResource SubtleTextBrush}" FontSize="12" Margin="0,2,0,0"
|
||||||
Text="Enter = heel scherm · Esc of rechtermuisknop = annuleren" />
|
Text="Enter = heel scherm · Esc of rechtermuisknop = annuleren" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public partial class RegionCaptureWindow : Window
|
|||||||
private readonly Screen _screen;
|
private readonly Screen _screen;
|
||||||
private readonly BitmapSource _frozen;
|
private readonly BitmapSource _frozen;
|
||||||
private readonly IReadOnlyList<Rectangle>? _windows;
|
private readonly IReadOnlyList<Rectangle>? _windows;
|
||||||
|
private readonly bool _showMagnifier;
|
||||||
private Point? _dragStart;
|
private Point? _dragStart;
|
||||||
private bool _dragging;
|
private bool _dragging;
|
||||||
private Rectangle? _hover;
|
private Rectangle? _hover;
|
||||||
@@ -33,13 +34,16 @@ public partial class RegionCaptureWindow : Window
|
|||||||
internal Rectangle? SelectedArea { get; private set; }
|
internal Rectangle? SelectedArea { get; private set; }
|
||||||
|
|
||||||
/// <param name="windows">Vensterrechthoeken van boven naar onder; gezet = venster aanklikken in plaats van slepen.</param>
|
/// <param name="windows">Vensterrechthoeken van boven naar onder; gezet = venster aanklikken in plaats van slepen.</param>
|
||||||
internal RegionCaptureWindow(Screen screen, BitmapSource frozen, IReadOnlyList<Rectangle>? windows = null)
|
/// <param name="showMagnifier">Vergrootglas bij de muisaanwijzer tonen; niet bij venster aanklikken.</param>
|
||||||
|
internal RegionCaptureWindow(Screen screen, BitmapSource frozen, IReadOnlyList<Rectangle>? windows = null, bool showMagnifier = false)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_screen = screen;
|
_screen = screen;
|
||||||
_frozen = frozen;
|
_frozen = frozen;
|
||||||
_windows = windows;
|
_windows = windows;
|
||||||
|
_showMagnifier = showMagnifier && windows == null;
|
||||||
Frozen.Source = frozen;
|
Frozen.Source = frozen;
|
||||||
|
MouseLeave += (_, _) => Magnifier.Hide();
|
||||||
if (windows != null)
|
if (windows != null)
|
||||||
{
|
{
|
||||||
Cursor = Cursors.Hand;
|
Cursor = Cursors.Hand;
|
||||||
@@ -78,12 +82,19 @@ public partial class RegionCaptureWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Point position = e.GetPosition(this);
|
||||||
|
if (_showMagnifier)
|
||||||
|
{
|
||||||
|
System.Drawing.Point physical = ToPhysical(position);
|
||||||
|
var relative = new System.Drawing.Point(physical.X - _screen.Bounds.X, physical.Y - _screen.Bounds.Y);
|
||||||
|
Magnifier.Show(_frozen, relative, physical, position, new Size(ActualWidth, ActualHeight));
|
||||||
|
}
|
||||||
|
|
||||||
if (_dragStart is not Point start || e.LeftButton != MouseButtonState.Pressed)
|
if (_dragStart is not Point start || e.LeftButton != MouseButtonState.Pressed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Point position = e.GetPosition(this);
|
|
||||||
if (!_dragging && (position - start).Length >= DragThreshold)
|
if (!_dragging && (position - start).Length >= DragThreshold)
|
||||||
{
|
{
|
||||||
_dragging = true;
|
_dragging = true;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
using Kniip.Editor.Annotations;
|
||||||
|
|
||||||
|
namespace Kniip.Editor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gekopieerde annotatie, gedeeld tussen editorvensters. Op het Windows-klembord staat alleen een markering,
|
||||||
|
/// zodat iets wat daarna in een ander programma wordt gekopieerd bij plakken voorgaat.
|
||||||
|
/// </summary>
|
||||||
|
internal static class AnnotationClipboard
|
||||||
|
{
|
||||||
|
private const string MarkerFormat = "Kniip.Annotation";
|
||||||
|
|
||||||
|
private static Annotation? _item;
|
||||||
|
private static string? _token;
|
||||||
|
|
||||||
|
public static bool Set(Annotation item)
|
||||||
|
{
|
||||||
|
string token = Guid.NewGuid().ToString("N");
|
||||||
|
if (!ClipboardWriter.SetMarker(MarkerFormat, token))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_item = item.Clone();
|
||||||
|
_token = token;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Een kopie van de annotatie, of null als het klembord inmiddels iets anders bevat.</summary>
|
||||||
|
public static Annotation? Get()
|
||||||
|
{
|
||||||
|
if (_item == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Clipboard.GetData(MarkerFormat) as string == _token ? _item.Clone() : null;
|
||||||
|
}
|
||||||
|
catch (ExternalException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ namespace Kniip.Editor;
|
|||||||
public sealed class AnnotationSurface : FrameworkElement
|
public sealed class AnnotationSurface : FrameworkElement
|
||||||
{
|
{
|
||||||
private static readonly Brush AccentBrush = RenderHelpers.Freeze(new SolidColorBrush(Palette.Accent));
|
private static readonly Brush AccentBrush = RenderHelpers.Freeze(new SolidColorBrush(Palette.Accent));
|
||||||
|
private static readonly Brush GhostHandleBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(170, 255, 255, 255)));
|
||||||
private static readonly Brush CropShadeBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(120, 0, 0, 0)));
|
private static readonly Brush CropShadeBrush = RenderHelpers.Freeze(new SolidColorBrush(Color.FromArgb(120, 0, 0, 0)));
|
||||||
|
|
||||||
public EditorDocument? Document { get; set; }
|
public EditorDocument? Document { get; set; }
|
||||||
@@ -74,8 +75,16 @@ public sealed class AnnotationSurface : FrameworkElement
|
|||||||
|
|
||||||
double size = HandleSize;
|
double size = HandleSize;
|
||||||
var pen = new Pen(AccentBrush, 1.2 / ViewScale);
|
var pen = new Pen(AccentBrush, 1.2 / ViewScale);
|
||||||
foreach (Point h in handles)
|
for (int i = 0; i < handles.Count; i++)
|
||||||
{
|
{
|
||||||
|
Point h = handles[i];
|
||||||
|
if (item.IsGhostHandle(i))
|
||||||
|
{
|
||||||
|
double radius = size * 0.35;
|
||||||
|
dc.DrawEllipse(GhostHandleBrush, pen, h, radius, radius);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
dc.DrawRoundedRectangle(Brushes.White, pen, new Rect(h.X - size / 2, h.Y - size / 2, size, size),
|
dc.DrawRoundedRectangle(Brushes.White, pen, new Rect(h.X - size / 2, h.Y - size / 2, size, size),
|
||||||
1.5 / ViewScale, 1.5 / ViewScale);
|
1.5 / ViewScale, 1.5 / ViewScale);
|
||||||
}
|
}
|
||||||
@@ -90,14 +99,20 @@ public sealed class AnnotationSurface : FrameworkElement
|
|||||||
{
|
{
|
||||||
IReadOnlyList<Point> handles = item.GetHandles();
|
IReadOnlyList<Point> handles = item.GetHandles();
|
||||||
double tolerance = HandleSize / 2 + 2 / ViewScale;
|
double tolerance = HandleSize / 2 + 2 / ViewScale;
|
||||||
|
int ghost = -1;
|
||||||
for (int i = handles.Count - 1; i >= 0; i--)
|
for (int i = handles.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
if (System.Math.Abs(handles[i].X - p.X) <= tolerance && System.Math.Abs(handles[i].Y - p.Y) <= tolerance)
|
if (System.Math.Abs(handles[i].X - p.X) <= tolerance && System.Math.Abs(handles[i].Y - p.Y) <= tolerance)
|
||||||
|
{
|
||||||
|
if (!item.IsGhostHandle(i))
|
||||||
{
|
{
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ghost = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return ghost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,9 @@ public abstract class Annotation
|
|||||||
|
|
||||||
public virtual IReadOnlyList<Point> GetHandles() => Array.Empty<Point>();
|
public virtual IReadOnlyList<Point> GetHandles() => Array.Empty<Point>();
|
||||||
|
|
||||||
|
/// <summary>Een greep die pas iets wordt als je hem sleept, zoals het middenpunt om een lijn te buigen.</summary>
|
||||||
|
public virtual bool IsGhostHandle(int index) => false;
|
||||||
|
|
||||||
public virtual void BeginHandleDrag(int index)
|
public virtual void BeginHandleDrag(int index)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -41,6 +44,8 @@ public abstract class Annotation
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual bool RemoveHandle(int index) => false;
|
||||||
|
|
||||||
/// <summary>Te klein om te bewaren na het tekenen (bijv. een losse klik met het rechthoek-gereedschap).</summary>
|
/// <summary>Te klein om te bewaren na het tekenen (bijv. een losse klik met het rechthoek-gereedschap).</summary>
|
||||||
public virtual bool IsDegenerate => false;
|
public virtual bool IsDegenerate => false;
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,16 @@ using System.Windows.Media;
|
|||||||
|
|
||||||
namespace Kniip.Editor.Annotations;
|
namespace Kniip.Editor.Annotations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lijn of pijl. Met buigpunten wordt het een vloeiende kromme die door alle punten loopt.
|
||||||
|
/// </summary>
|
||||||
public sealed class LineAnnotation : Annotation
|
public sealed class LineAnnotation : Annotation
|
||||||
{
|
{
|
||||||
|
private const int SamplesPerSegment = 16;
|
||||||
|
|
||||||
|
private int _dragBend = -1;
|
||||||
|
private int _pendingGhost = -1;
|
||||||
|
|
||||||
public LineAnnotation(bool isArrow)
|
public LineAnnotation(bool isArrow)
|
||||||
{
|
{
|
||||||
IsArrow = isArrow;
|
IsArrow = isArrow;
|
||||||
@@ -18,27 +26,124 @@ public sealed class LineAnnotation : Annotation
|
|||||||
|
|
||||||
public Point End { get; set; }
|
public Point End { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Tussenpunten waar de kromme doorheen loopt, in volgorde van begin naar eind.</summary>
|
||||||
|
public List<Point> Bends { get; set; } = new();
|
||||||
|
|
||||||
public override ToolKind Kind => IsArrow ? ToolKind.Arrow : ToolKind.Line;
|
public override ToolKind Kind => IsArrow ? ToolKind.Arrow : ToolKind.Line;
|
||||||
|
|
||||||
public override Rect Bounds
|
public override Rect Bounds
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var r = new Rect(Start, End);
|
List<Point> points = Flatten();
|
||||||
|
var r = new Rect(points[0], points[0]);
|
||||||
|
foreach (Point p in points)
|
||||||
|
{
|
||||||
|
r.Union(p);
|
||||||
|
}
|
||||||
|
|
||||||
double pad = Style.Thickness / 2 + (IsArrow ? HeadLength / 2 : 0);
|
double pad = Style.Thickness / 2 + (IsArrow ? HeadLength / 2 : 0);
|
||||||
r.Inflate(pad, pad);
|
r.Inflate(pad, pad);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsDegenerate => (End - Start).Length < 4;
|
public override bool IsDegenerate => Bends.Count == 0 && (End - Start).Length < 4;
|
||||||
|
|
||||||
private double HeadLength => Math.Max(12, Style.Thickness * 4);
|
private double HeadLength => Math.Max(12, Style.Thickness * 4);
|
||||||
|
|
||||||
|
private readonly record struct Segment(Point A, Point C1, Point C2, Point B)
|
||||||
|
{
|
||||||
|
public Point At(double t)
|
||||||
|
{
|
||||||
|
double u = 1 - t;
|
||||||
|
double a = u * u * u, b = 3 * u * u * t, c = 3 * u * t * t, d = t * t * t;
|
||||||
|
return new Point(
|
||||||
|
a * A.X + b * C1.X + c * C2.X + d * B.X,
|
||||||
|
a * A.Y + b * C1.Y + c * C2.Y + d * B.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Point> ThroughPoints()
|
||||||
|
{
|
||||||
|
var points = new List<Point>(Bends.Count + 2) { Start };
|
||||||
|
points.AddRange(Bends);
|
||||||
|
points.Add(End);
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Catmull-Rom door alle punten, omgezet naar kubische Bezier-segmenten.</summary>
|
||||||
|
private List<Segment> Segments()
|
||||||
|
{
|
||||||
|
List<Point> p = ThroughPoints();
|
||||||
|
int last = p.Count - 1;
|
||||||
|
var segments = new List<Segment>(last);
|
||||||
|
for (int i = 0; i < last; i++)
|
||||||
|
{
|
||||||
|
Point prev = p[Math.Max(i - 1, 0)];
|
||||||
|
Point next = p[Math.Min(i + 2, last)];
|
||||||
|
segments.Add(new Segment(p[i], p[i] + (p[i + 1] - prev) / 6, p[i + 1] - (next - p[i]) / 6, p[i + 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Point> Flatten()
|
||||||
|
{
|
||||||
|
if (Bends.Count == 0)
|
||||||
|
{
|
||||||
|
return new List<Point> { Start, End };
|
||||||
|
}
|
||||||
|
|
||||||
|
var points = new List<Point> { Start };
|
||||||
|
foreach (Segment s in Segments())
|
||||||
|
{
|
||||||
|
for (int k = 1; k <= SamplesPerSegment; k++)
|
||||||
|
{
|
||||||
|
points.Add(s.At(k / (double)SamplesPerSegment));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double PathLength(List<Point> points)
|
||||||
|
{
|
||||||
|
double length = 0;
|
||||||
|
for (int i = 1; i < points.Count; i++)
|
||||||
|
{
|
||||||
|
length += (points[i] - points[i - 1]).Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Geometry BuildGeometry(List<Segment> segments)
|
||||||
|
{
|
||||||
|
var geometry = new StreamGeometry();
|
||||||
|
using (StreamGeometryContext ctx = geometry.Open())
|
||||||
|
{
|
||||||
|
ctx.BeginFigure(segments[0].A, false, false);
|
||||||
|
foreach (Segment s in segments)
|
||||||
|
{
|
||||||
|
if (Bends.Count == 0)
|
||||||
|
{
|
||||||
|
ctx.LineTo(s.B, true, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.BezierTo(s.C1, s.C2, s.B, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.Freeze();
|
||||||
|
return geometry;
|
||||||
|
}
|
||||||
|
|
||||||
public override void Render(DrawingContext dc, EditorDocument document)
|
public override void Render(DrawingContext dc, EditorDocument document)
|
||||||
{
|
{
|
||||||
Vector v = End - Start;
|
double length = PathLength(Flatten());
|
||||||
double length = v.Length;
|
|
||||||
if (length < 1)
|
if (length < 1)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -50,18 +155,26 @@ public sealed class LineAnnotation : Annotation
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var geometry = new GeometryGroup { FillRule = FillRule.Nonzero };
|
List<Segment> segments = Segments();
|
||||||
Brush? headFill = null;
|
|
||||||
|
|
||||||
if (IsArrow)
|
if (IsArrow)
|
||||||
{
|
{
|
||||||
|
Segment lastSegment = segments[^1];
|
||||||
|
Vector v = lastSegment.B - lastSegment.C2;
|
||||||
|
if (v.Length < 1e-6)
|
||||||
|
{
|
||||||
|
v = lastSegment.B - lastSegment.A;
|
||||||
|
}
|
||||||
|
|
||||||
v.Normalize();
|
v.Normalize();
|
||||||
double headLength = Math.Min(HeadLength, length);
|
double headLength = Math.Min(HeadLength, length);
|
||||||
double halfWidth = headLength * 0.45;
|
double halfWidth = headLength * 0.45;
|
||||||
Point basePoint = End - v * headLength;
|
Point basePoint = End - v * headLength;
|
||||||
var normal = new Vector(-v.Y, v.X);
|
var normal = new Vector(-v.Y, v.X);
|
||||||
|
|
||||||
geometry.Children.Add(new LineGeometry(Start, basePoint + v * Math.Min(2, headLength / 4)));
|
Point lineEnd = basePoint + v * Math.Min(2, headLength / 4);
|
||||||
|
Vector shift = lineEnd - End;
|
||||||
|
segments[^1] = lastSegment with { C2 = lastSegment.C2 + shift, B = lineEnd };
|
||||||
|
|
||||||
var head = new StreamGeometry();
|
var head = new StreamGeometry();
|
||||||
using (StreamGeometryContext ctx = head.Open())
|
using (StreamGeometryContext ctx = head.Open())
|
||||||
@@ -72,39 +185,105 @@ public sealed class LineAnnotation : Annotation
|
|||||||
}
|
}
|
||||||
|
|
||||||
head.Freeze();
|
head.Freeze();
|
||||||
headFill = RenderHelpers.BrushOrNull(Style.Stroke);
|
Brush? headFill = RenderHelpers.BrushOrNull(Style.Stroke);
|
||||||
|
|
||||||
// Lijn en punt apart tekenen zodat de punt gevuld en niet gestippeld wordt.
|
// Lijn en punt apart tekenen zodat de punt gevuld en niet gestippeld wordt.
|
||||||
RenderHelpers.DrawWithShadow(dc, geometry, null, pen, Style.Shadow);
|
RenderHelpers.DrawWithShadow(dc, BuildGeometry(segments), null, pen, Style.Shadow);
|
||||||
var headPen = RenderHelpers.CreatePen(Style.Stroke, Math.Max(1, Style.Thickness / 3), LineDash.Solid);
|
var headPen = RenderHelpers.CreatePen(Style.Stroke, Math.Max(1, Style.Thickness / 3), LineDash.Solid);
|
||||||
RenderHelpers.DrawWithShadow(dc, head, headFill, headPen, Style.Shadow);
|
RenderHelpers.DrawWithShadow(dc, head, headFill, headPen, Style.Shadow);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
geometry.Children.Add(new LineGeometry(Start, End));
|
RenderHelpers.DrawWithShadow(dc, BuildGeometry(segments), null, pen, Style.Shadow);
|
||||||
RenderHelpers.DrawWithShadow(dc, geometry, null, pen, Style.Shadow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool HitTest(Point point, double tolerance) =>
|
public override bool HitTest(Point point, double tolerance)
|
||||||
RenderHelpers.DistanceToSegment(point, Start, End) <= tolerance + Style.Thickness / 2 + (IsArrow ? 4 : 0);
|
{
|
||||||
|
double limit = tolerance + Style.Thickness / 2 + (IsArrow ? 4 : 0);
|
||||||
|
List<Point> points = Flatten();
|
||||||
|
for (int i = 1; i < points.Count; i++)
|
||||||
|
{
|
||||||
|
if (RenderHelpers.DistanceToSegment(point, points[i - 1], points[i]) <= limit)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public override void Move(Vector delta)
|
public override void Move(Vector delta)
|
||||||
{
|
{
|
||||||
Start += delta;
|
Start += delta;
|
||||||
End += delta;
|
End += delta;
|
||||||
|
for (int i = 0; i < Bends.Count; i++)
|
||||||
|
{
|
||||||
|
Bends[i] += delta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IReadOnlyList<Point> GetHandles() => new[] { Start, End };
|
/// <summary>Volgorde: begin, eind, buigpunten, en daarna één spook-greep midden op elk segment.</summary>
|
||||||
|
public override IReadOnlyList<Point> GetHandles()
|
||||||
|
{
|
||||||
|
var handles = new List<Point> { Start, End };
|
||||||
|
handles.AddRange(Bends);
|
||||||
|
foreach (Segment s in Segments())
|
||||||
|
{
|
||||||
|
handles.Add(s.At(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
return handles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool IsGhostHandle(int index) => index >= 2 + Bends.Count;
|
||||||
|
|
||||||
|
public override void BeginHandleDrag(int index)
|
||||||
|
{
|
||||||
|
_pendingGhost = IsGhostHandle(index) ? index - 2 - Bends.Count : -1;
|
||||||
|
_dragBend = index >= 2 && _pendingGhost < 0 ? index - 2 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
public override void MoveHandle(int index, Point point, bool constrain)
|
public override void MoveHandle(int index, Point point, bool constrain)
|
||||||
{
|
{
|
||||||
if (index == 0)
|
// Het buigpunt pas bij de eerste beweging invoegen, zodat een losse klik de lijn niet verandert.
|
||||||
|
if (_pendingGhost >= 0)
|
||||||
{
|
{
|
||||||
Start = constrain ? RenderHelpers.SnapAngle(End, point) : point;
|
Bends.Insert(_pendingGhost, point);
|
||||||
|
_dragBend = _pendingGhost;
|
||||||
|
_pendingGhost = -1;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (_dragBend >= 0)
|
||||||
{
|
{
|
||||||
End = constrain ? RenderHelpers.SnapAngle(Start, point) : point;
|
Bends[_dragBend] = point;
|
||||||
}
|
}
|
||||||
|
else if (index == 0)
|
||||||
|
{
|
||||||
|
Start = constrain ? RenderHelpers.SnapAngle(Bends.Count > 0 ? Bends[0] : End, point) : point;
|
||||||
|
}
|
||||||
|
else if (index == 1)
|
||||||
|
{
|
||||||
|
End = constrain ? RenderHelpers.SnapAngle(Bends.Count > 0 ? Bends[^1] : Start, point) : point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool RemoveHandle(int index)
|
||||||
|
{
|
||||||
|
int bend = index - 2;
|
||||||
|
if (bend < 0 || bend >= Bends.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bends.RemoveAt(bend);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Annotation Clone()
|
||||||
|
{
|
||||||
|
var clone = (LineAnnotation)base.Clone();
|
||||||
|
clone.Bends = new List<Point>(Bends);
|
||||||
|
return clone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,25 +14,25 @@
|
|||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ToggleButton x:Name="Toggle" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30"
|
<ToggleButton x:Name="Toggle" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30"
|
||||||
BorderBrush="#E0E0E0" Background="White">
|
BorderBrush="{DynamicResource ControlBorderBrush}" Background="{DynamicResource ControlBrush}">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Grid Width="20" Height="20">
|
<Grid Width="20" Height="20">
|
||||||
<Border x:Name="CheckerBack" CornerRadius="4" Background="{StaticResource Checker}" Visibility="Collapsed" />
|
<Border x:Name="CheckerBack" CornerRadius="4" Background="{StaticResource Checker}" Visibility="Collapsed" />
|
||||||
<Border x:Name="Swatch" CornerRadius="4" BorderThickness="0" />
|
<Border x:Name="Swatch" CornerRadius="4" BorderThickness="0" />
|
||||||
<Border x:Name="OutlineHole" CornerRadius="1.5" Margin="5" Background="White" Visibility="Collapsed" />
|
<Border x:Name="OutlineHole" CornerRadius="1.5" Margin="5" Background="{DynamicResource ControlBrush}" Visibility="Collapsed" />
|
||||||
<Grid x:Name="TextGlyph" Background="White" Visibility="Collapsed">
|
<Grid x:Name="TextGlyph" Background="{DynamicResource ControlBrush}" Visibility="Collapsed">
|
||||||
<TextBlock Text="A" FontSize="14" HorizontalAlignment="Center" Margin="0,-1,0,0"
|
<TextBlock Text="A" FontSize="14" HorizontalAlignment="Center" Margin="0,-1,0,0"
|
||||||
Foreground="{StaticResource InkBrush}" />
|
Foreground="{DynamicResource InkBrush}" />
|
||||||
<Border x:Name="TextBar" Height="4" CornerRadius="1" VerticalAlignment="Bottom" Margin="2,0" />
|
<Border x:Name="TextBar" Height="4" CornerRadius="1" VerticalAlignment="Bottom" Margin="2,0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="9" Margin="5,0,1,0" Foreground="#5C5C5C" />
|
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="9" Margin="5,0,1,0" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<Popup x:Name="PopupPanel" PlacementTarget="{Binding ElementName=Toggle}" Placement="Bottom" StaysOpen="False"
|
<Popup x:Name="PopupPanel" PlacementTarget="{Binding ElementName=Toggle}" Placement="Bottom" StaysOpen="False"
|
||||||
AllowsTransparency="True" PopupAnimation="Fade"
|
AllowsTransparency="True" PopupAnimation="Fade"
|
||||||
IsOpen="{Binding IsChecked, ElementName=Toggle, Mode=TwoWay}">
|
IsOpen="{Binding IsChecked, ElementName=Toggle, Mode=TwoWay}">
|
||||||
<Border Background="White" BorderBrush="#D6D6D6" BorderThickness="1" CornerRadius="6" Padding="8" Margin="0,4,10,10">
|
<Border Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1" CornerRadius="6" Padding="8" Margin="0,4,10,10">
|
||||||
<Border.Effect>
|
<Border.Effect>
|
||||||
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.18" />
|
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.18" />
|
||||||
</Border.Effect>
|
</Border.Effect>
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ public sealed class EditorDocument
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Vervangt het knipsel door de platgemaakte versie; de annotaties zitten daarna in de pixels.</summary>
|
||||||
|
public void Flatten(BitmapSource rendered)
|
||||||
|
{
|
||||||
|
SetPixels(PixelUtil.ToOpaqueBgra(rendered), rendered.PixelWidth, rendered.PixelHeight);
|
||||||
|
Items = new List<Annotation>();
|
||||||
|
}
|
||||||
|
|
||||||
public string GetStepLabel(StepAnnotation step)
|
public string GetStepLabel(StepAnnotation step)
|
||||||
{
|
{
|
||||||
int ordinal = 0;
|
int ordinal = 0;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:Kniip.Editor"
|
xmlns:local="clr-namespace:Kniip.Editor"
|
||||||
Title="kniip" Width="1180" Height="780" MinWidth="760" MinHeight="420"
|
Title="kniip" Width="1180" Height="780" MinWidth="760" MinHeight="420"
|
||||||
WindowStartupLocation="CenterScreen" Background="{StaticResource WindowBrush}"
|
WindowStartupLocation="CenterScreen" Background="{DynamicResource WindowBrush}" Foreground="{DynamicResource InkBrush}"
|
||||||
FontFamily="Segoe UI" FontSize="13" UseLayoutRounding="True"
|
FontFamily="Segoe UI" FontSize="13" UseLayoutRounding="True"
|
||||||
Icon="pack://application:,,,/AppIcon.ico">
|
Icon="pack://application:,,,/AppIcon.ico"
|
||||||
|
AllowDrop="True" PreviewDragEnter="Window_PreviewDragOver" PreviewDragOver="Window_PreviewDragOver" PreviewDrop="Window_PreviewDrop">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<local:CornerPreviewConverter x:Key="CornerPreview" />
|
<local:CornerPreviewConverter x:Key="CornerPreview" />
|
||||||
<local:CursorIconConverter x:Key="CursorIcon" />
|
<local:CursorIconConverter x:Key="CursorIcon" />
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<DataTemplate x:Key="ThicknessTemplate">
|
<DataTemplate x:Key="ThicknessTemplate">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Grid Width="16" Height="16" VerticalAlignment="Center">
|
<Grid Width="16" Height="16" VerticalAlignment="Center">
|
||||||
<Ellipse x:Name="Dot" Fill="{StaticResource InkBrush}" Width="{Binding}" Height="{Binding}"
|
<Ellipse x:Name="Dot" Fill="{DynamicResource InkBrush}" Width="{Binding}" Height="{Binding}"
|
||||||
MinWidth="2" MinHeight="2" MaxWidth="15" MaxHeight="15"
|
MinWidth="2" MinHeight="2" MaxWidth="15" MaxHeight="15"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
<Line x:Name="NoneLine" X1="3" Y1="13" X2="13" Y2="3" Stroke="#E81123" StrokeThickness="1.5" Visibility="Collapsed" />
|
<Line x:Name="NoneLine" X1="3" Y1="13" X2="13" Y2="3" Stroke="#E81123" StrokeThickness="1.5" Visibility="Collapsed" />
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
<!-- Lijnstijl: voorbeeldlijn -->
|
<!-- Lijnstijl: voorbeeldlijn -->
|
||||||
<DataTemplate x:Key="DashTemplate">
|
<DataTemplate x:Key="DashTemplate">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Line x:Name="Sample" X1="1" Y1="8" X2="35" Y2="8" Width="36" Height="16" Stroke="{StaticResource InkBrush}"
|
<Line x:Name="Sample" X1="1" Y1="8" X2="35" Y2="8" Width="36" Height="16" Stroke="{DynamicResource InkBrush}"
|
||||||
StrokeThickness="2" VerticalAlignment="Center" />
|
StrokeThickness="2" VerticalAlignment="Center" />
|
||||||
<TextBlock x:Name="Label" Text="Doorgetrokken" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
<TextBlock x:Name="Label" Text="Doorgetrokken" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Grid Width="18" Height="18" VerticalAlignment="Center" ClipToBounds="True">
|
<Grid Width="18" Height="18" VerticalAlignment="Center" ClipToBounds="True">
|
||||||
<Border Width="26" Height="26" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="2,2,0,0"
|
<Border Width="26" Height="26" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="2,2,0,0"
|
||||||
BorderBrush="{StaticResource InkBrush}" BorderThickness="2,2,0,0"
|
BorderBrush="{DynamicResource InkBrush}" BorderThickness="2,2,0,0"
|
||||||
CornerRadius="{Binding Converter={StaticResource CornerPreview}}" />
|
CornerRadius="{Binding Converter={StaticResource CornerPreview}}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<TextBlock x:Name="Label" Text="{Binding StringFormat={}{0} px}" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
<TextBlock x:Name="Label" Text="{Binding StringFormat={}{0} px}" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
||||||
@@ -83,7 +84,7 @@
|
|||||||
<DataTemplate x:Key="FontSizeTemplate">
|
<DataTemplate x:Key="FontSizeTemplate">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="A" FontSize="15" VerticalAlignment="Center" />
|
<TextBlock Text="A" FontSize="15" VerticalAlignment="Center" />
|
||||||
<TextBlock Text="{Binding}" FontSize="11" Margin="3,4,0,0" VerticalAlignment="Center" Foreground="{StaticResource SubtleTextBrush}" />
|
<TextBlock Text="{Binding}" FontSize="11" Margin="3,4,0,0" VerticalAlignment="Center" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
@@ -91,10 +92,10 @@
|
|||||||
<DataTemplate x:Key="SizeTemplate">
|
<DataTemplate x:Key="SizeTemplate">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Grid Width="16" Height="16" VerticalAlignment="Center">
|
<Grid Width="16" Height="16" VerticalAlignment="Center">
|
||||||
<Ellipse Stroke="{StaticResource InkBrush}" StrokeThickness="1" />
|
<Ellipse Stroke="{DynamicResource InkBrush}" StrokeThickness="1" />
|
||||||
<Path Data="M5,8 H11 M8,5 V11" Stroke="{StaticResource InkBrush}" StrokeThickness="1" />
|
<Path Data="M5,8 H11 M8,5 V11" Stroke="{DynamicResource InkBrush}" StrokeThickness="1" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<TextBlock Text="{Binding}" FontSize="11" Margin="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource SubtleTextBrush}" />
|
<TextBlock Text="{Binding}" FontSize="11" Margin="4,0,0,0" VerticalAlignment="Center" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@
|
|||||||
<!-- Soort cursor: het cursoricoon zelf -->
|
<!-- Soort cursor: het cursoricoon zelf -->
|
||||||
<DataTemplate x:Key="CursorKindTemplate">
|
<DataTemplate x:Key="CursorKindTemplate">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Path Data="{Binding Converter={StaticResource CursorIcon}}" Fill="White" Stroke="{StaticResource InkBrush}"
|
<Path Data="{Binding Converter={StaticResource CursorIcon}}" Fill="{DynamicResource ControlBrush}" Stroke="{DynamicResource InkBrush}"
|
||||||
StrokeThickness="1" StrokeLineJoin="Round" Stretch="Uniform" Width="16" Height="18" VerticalAlignment="Center" />
|
StrokeThickness="1" StrokeLineJoin="Round" Stretch="Uniform" Width="16" Height="18" VerticalAlignment="Center" />
|
||||||
<TextBlock x:Name="Label" Text="Pijl" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
<TextBlock x:Name="Label" Text="Pijl" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -151,16 +152,16 @@
|
|||||||
<DataTemplate x:Key="BlurModeTemplate">
|
<DataTemplate x:Key="BlurModeTemplate">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Grid Width="16" Height="16" VerticalAlignment="Center">
|
<Grid Width="16" Height="16" VerticalAlignment="Center">
|
||||||
<Path x:Name="PixelIcon" Fill="{StaticResource InkBrush}"
|
<Path x:Name="PixelIcon" Fill="{DynamicResource InkBrush}"
|
||||||
Data="M1,1 h4.5 v4.5 h-4.5 z M10.5,1 h4.5 v4.5 h-4.5 z M5.75,5.75 h4.5 v4.5 h-4.5 z M1,10.5 h4.5 v4.5 h-4.5 z M10.5,10.5 h4.5 v4.5 h-4.5 z" />
|
Data="M1,1 h4.5 v4.5 h-4.5 z M10.5,1 h4.5 v4.5 h-4.5 z M5.75,5.75 h4.5 v4.5 h-4.5 z M1,10.5 h4.5 v4.5 h-4.5 z M10.5,10.5 h4.5 v4.5 h-4.5 z" />
|
||||||
<Ellipse x:Name="BlurIcon" Visibility="Collapsed">
|
<Ellipse x:Name="BlurIcon" Visibility="Collapsed" Fill="{DynamicResource InkBrush}">
|
||||||
<Ellipse.Fill>
|
<Ellipse.OpacityMask>
|
||||||
<RadialGradientBrush>
|
<RadialGradientBrush>
|
||||||
<GradientStop Color="#FF1A1A1A" Offset="0" />
|
<GradientStop Color="#FF000000" Offset="0" />
|
||||||
<GradientStop Color="#801A1A1A" Offset="0.5" />
|
<GradientStop Color="#80000000" Offset="0.5" />
|
||||||
<GradientStop Color="#001A1A1A" Offset="1" />
|
<GradientStop Color="#00000000" Offset="1" />
|
||||||
</RadialGradientBrush>
|
</RadialGradientBrush>
|
||||||
</Ellipse.Fill>
|
</Ellipse.OpacityMask>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
</Grid>
|
</Grid>
|
||||||
<TextBlock x:Name="Label" Text="Pixeleren" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
<TextBlock x:Name="Label" Text="Pixeleren" Margin="8,0,0,0" VerticalAlignment="Center" FontSize="12" />
|
||||||
@@ -179,16 +180,26 @@
|
|||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<!-- Statusbalk -->
|
<!-- Statusbalk -->
|
||||||
<Border DockPanel.Dock="Bottom" Background="{StaticResource ToolbarBrush}" BorderBrush="{StaticResource DividerBrush}"
|
<Border DockPanel.Dock="Bottom" Background="{DynamicResource ToolbarBrush}" BorderBrush="{DynamicResource DividerBrush}"
|
||||||
BorderThickness="0,1,0,0" Padding="12,4">
|
BorderThickness="0,1,0,0" Padding="12,4">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock x:Name="InfoText" DockPanel.Dock="Right" Foreground="{StaticResource SubtleTextBrush}" FontSize="12" />
|
<TextBlock x:Name="InfoText" DockPanel.Dock="Right" Foreground="{DynamicResource SubtleTextBrush}" FontSize="12" />
|
||||||
<TextBlock x:Name="HintText" Foreground="{StaticResource SubtleTextBrush}" FontSize="12" TextTrimming="CharacterEllipsis" />
|
<TextBlock x:Name="HintText" Foreground="{DynamicResource SubtleTextBrush}" FontSize="12" TextTrimming="CharacterEllipsis" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Onderste werkbalk in de gesplitste indeling; de code verplaatst de acties hierheen -->
|
||||||
|
<Border x:Name="ActionBar" DockPanel.Dock="Bottom" Background="{DynamicResource ToolbarBrush}" BorderBrush="{DynamicResource DividerBrush}"
|
||||||
|
BorderThickness="0,1,0,0" Padding="8,6" Visibility="Collapsed"
|
||||||
|
SizeChanged="Toolbar_SizeChanged" ContextMenuOpening="Toolbar_ContextMenuOpening">
|
||||||
|
<Border.ContextMenu>
|
||||||
|
<ContextMenu x:Name="ActionBarMenu" />
|
||||||
|
</Border.ContextMenu>
|
||||||
|
<local:ToolbarPanel x:Name="ActionItems" OrderChanged="ToolbarItems_OrderChanged" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Gereedschappen en acties; rechtsklik voor vergrendelen, positie en zichtbare knoppen -->
|
<!-- Gereedschappen en acties; rechtsklik voor vergrendelen, positie en zichtbare knoppen -->
|
||||||
<Border x:Name="Toolbar" DockPanel.Dock="Top" Background="{StaticResource ToolbarBrush}" BorderBrush="{StaticResource DividerBrush}"
|
<Border x:Name="Toolbar" DockPanel.Dock="Top" Background="{DynamicResource ToolbarBrush}" BorderBrush="{DynamicResource DividerBrush}"
|
||||||
BorderThickness="0,0,0,1" Padding="8,6" SizeChanged="Toolbar_SizeChanged" ContextMenuOpening="Toolbar_ContextMenuOpening">
|
BorderThickness="0,0,0,1" Padding="8,6" SizeChanged="Toolbar_SizeChanged" ContextMenuOpening="Toolbar_ContextMenuOpening">
|
||||||
<Border.ContextMenu>
|
<Border.ContextMenu>
|
||||||
<ContextMenu x:Name="ToolbarMenu" />
|
<ContextMenu x:Name="ToolbarMenu" />
|
||||||
@@ -235,7 +246,7 @@
|
|||||||
</RadioButton>
|
</RadioButton>
|
||||||
<RadioButton GroupName="Tools" Tag="Cursor" local:ToolbarPanel.ItemId="Cursor" local:ToolbarPanel.Title="Cursor"
|
<RadioButton GroupName="Tools" Tag="Cursor" local:ToolbarPanel.ItemId="Cursor" local:ToolbarPanel.Title="Cursor"
|
||||||
Style="{StaticResource ToolToggle}" ToolTip="Cursor plaatsen (C)" Checked="Tool_Checked">
|
Style="{StaticResource ToolToggle}" ToolTip="Cursor plaatsen (C)" Checked="Tool_Checked">
|
||||||
<Path Style="{StaticResource IconPath}" Fill="White"
|
<Path Style="{StaticResource IconPath}" Fill="{DynamicResource ControlBrush}"
|
||||||
Data="M7,4 L7,16 L9.8,13.4 L11.7,17.6 L13.6,16.8 L11.8,12.6 L15.4,12.6 Z M4,4 L2.5,2.5 M7,1 L7,0 M4,7 L2,7" />
|
Data="M7,4 L7,16 L9.8,13.4 L11.7,17.6 L13.6,16.8 L11.8,12.6 L15.4,12.6 Z M4,4 L2.5,2.5 M7,1 L7,0 M4,7 L2,7" />
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
<RadioButton GroupName="Tools" Tag="Emoji" local:ToolbarPanel.ItemId="Emoji" local:ToolbarPanel.Title="Emoji"
|
<RadioButton GroupName="Tools" Tag="Emoji" local:ToolbarPanel.ItemId="Emoji" local:ToolbarPanel.Title="Emoji"
|
||||||
@@ -251,7 +262,7 @@
|
|||||||
</RadioButton>
|
</RadioButton>
|
||||||
<RadioButton GroupName="Tools" Tag="Blur" local:ToolbarPanel.ItemId="Blur" local:ToolbarPanel.Title="Vervagen / pixeleren"
|
<RadioButton GroupName="Tools" Tag="Blur" local:ToolbarPanel.ItemId="Blur" local:ToolbarPanel.Title="Vervagen / pixeleren"
|
||||||
Style="{StaticResource ToolToggle}" ToolTip="Vervagen / pixeleren (B)" Checked="Tool_Checked">
|
Style="{StaticResource ToolToggle}" ToolTip="Vervagen / pixeleren (B)" Checked="Tool_Checked">
|
||||||
<Path Style="{StaticResource IconPath}" Fill="#1A1A1A" StrokeThickness="0"
|
<Path Style="{StaticResource IconPath}" Fill="{DynamicResource InkBrush}" StrokeThickness="0"
|
||||||
Data="M3,3 h4.5 v4.5 h-4.5 z M12.5,3 h4.5 v4.5 h-4.5 z M7.75,7.75 h4.5 v4.5 h-4.5 z M3,12.5 h4.5 v4.5 h-4.5 z M12.5,12.5 h4.5 v4.5 h-4.5 z" />
|
Data="M3,3 h4.5 v4.5 h-4.5 z M12.5,3 h4.5 v4.5 h-4.5 z M7.75,7.75 h4.5 v4.5 h-4.5 z M3,12.5 h4.5 v4.5 h-4.5 z M12.5,12.5 h4.5 v4.5 h-4.5 z" />
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
<local:ToolbarSeparator local:ToolbarPanel.ItemId="Sep2" />
|
<local:ToolbarSeparator local:ToolbarPanel.ItemId="Sep2" />
|
||||||
@@ -314,9 +325,14 @@
|
|||||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||||
</Button>
|
</Button>
|
||||||
<local:ToolbarSeparator local:ToolbarPanel.ItemId="Sep5" />
|
<local:ToolbarSeparator local:ToolbarPanel.ItemId="Sep5" />
|
||||||
<Button local:ToolbarPanel.ItemId="Settings" local:ToolbarPanel.Title="Instellingen"
|
<Button x:Name="SettingsButton" local:ToolbarPanel.ItemId="Settings" local:ToolbarPanel.Title="Instellingen"
|
||||||
Style="{StaticResource FlatButton}" ToolTip="Instellingen" Click="Settings_Click">
|
Style="{StaticResource FlatButton}" ToolTip="Instellingen" Click="Settings_Click">
|
||||||
|
<Grid>
|
||||||
<TextBlock Text="" Style="{StaticResource IconText}" />
|
<TextBlock Text="" Style="{StaticResource IconText}" />
|
||||||
|
<Ellipse x:Name="SettingsBadge" Width="8" Height="8" Fill="{DynamicResource AccentBrush}" Stroke="{DynamicResource ToolbarBrush}"
|
||||||
|
StrokeThickness="1" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,-2,-3,0"
|
||||||
|
IsHitTestVisible="False" Visibility="Collapsed" />
|
||||||
|
</Grid>
|
||||||
</Button>
|
</Button>
|
||||||
<Button x:Name="SaveButton" local:ToolbarPanel.ItemId="Save" local:ToolbarPanel.Title="Opslaan"
|
<Button x:Name="SaveButton" local:ToolbarPanel.ItemId="Save" local:ToolbarPanel.Title="Opslaan"
|
||||||
Style="{StaticResource SecondaryButton}" ToolTip="Opslaan (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als" Click="Save_Click">
|
Style="{StaticResource SecondaryButton}" ToolTip="Opslaan (Ctrl+S); Shift+klik of Ctrl+Shift+S = opslaan als" Click="Save_Click">
|
||||||
@@ -339,11 +355,15 @@
|
|||||||
<TextBlock x:Name="CopyLabel" Text="Kopiëren" Margin="6,0,0,0" />
|
<TextBlock x:Name="CopyLabel" Text="Kopiëren" Margin="6,0,0,0" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<!-- Alleen in de gesplitste indeling: gereedschappen in het midden, Instellingen rechts -->
|
||||||
|
<local:ToolbarSpacer local:ToolbarPanel.ItemId="SplitSpacer1" />
|
||||||
|
<local:ToolbarSpacer local:ToolbarPanel.ItemId="SplitSpacer2" />
|
||||||
</local:ToolbarPanel>
|
</local:ToolbarPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Eigenschappen van het actieve gereedschap of het geselecteerde object -->
|
<!-- Eigenschappen van het actieve gereedschap of het geselecteerde object -->
|
||||||
<Border DockPanel.Dock="Top" Background="White" BorderBrush="{StaticResource DividerBrush}" BorderThickness="0,0,0,1"
|
<Border x:Name="PropsBar" DockPanel.Dock="Top" Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource DividerBrush}" BorderThickness="0,0,0,1"
|
||||||
Padding="12,4" MinHeight="42">
|
Padding="12,4" MinHeight="42">
|
||||||
<WrapPanel x:Name="PropsPanel" VerticalAlignment="Center" HorizontalAlignment="Center">
|
<WrapPanel x:Name="PropsPanel" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
<StackPanel x:Name="StrokeGroup" Style="{StaticResource PropGroup}">
|
<StackPanel x:Name="StrokeGroup" Style="{StaticResource PropGroup}">
|
||||||
@@ -392,7 +412,7 @@
|
|||||||
<StackPanel x:Name="BlurGroup" Style="{StaticResource PropGroup}">
|
<StackPanel x:Name="BlurGroup" Style="{StaticResource PropGroup}">
|
||||||
<ComboBox x:Name="BlurModeCombo" Style="{StaticResource MiniCombo}" ItemTemplate="{StaticResource BlurModeTemplate}"
|
<ComboBox x:Name="BlurModeCombo" Style="{StaticResource MiniCombo}" ItemTemplate="{StaticResource BlurModeTemplate}"
|
||||||
ToolTip="Effect" SelectionChanged="BlurMode_Changed" />
|
ToolTip="Effect" SelectionChanged="BlurMode_Changed" />
|
||||||
<Border Background="White" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="4" Height="30" Margin="2,0" Padding="8,0"
|
<Border Background="{DynamicResource ControlBrush}" BorderBrush="{DynamicResource ControlBorderBrush}" BorderThickness="1" CornerRadius="4" Height="30" Margin="2,0" Padding="8,0"
|
||||||
ToolTip="Sterkte">
|
ToolTip="Sterkte">
|
||||||
<Slider x:Name="BlurStrengthSlider" Width="90" Minimum="1" Maximum="20" IsSnapToTickEnabled="True" TickFrequency="1"
|
<Slider x:Name="BlurStrengthSlider" Width="90" Minimum="1" Maximum="20" IsSnapToTickEnabled="True" TickFrequency="1"
|
||||||
VerticalAlignment="Center" ValueChanged="BlurStrength_Changed" />
|
VerticalAlignment="Center" ValueChanged="BlurStrength_Changed" />
|
||||||
@@ -401,7 +421,7 @@
|
|||||||
<StackPanel x:Name="StraightGroup" Style="{StaticResource PropGroup}">
|
<StackPanel x:Name="StraightGroup" Style="{StaticResource PropGroup}">
|
||||||
<ToggleButton x:Name="StraightCheck" Style="{StaticResource MiniToggle}" ToolTip="Rechte horizontale streep (Shift wisselt)"
|
<ToggleButton x:Name="StraightCheck" Style="{StaticResource MiniToggle}" ToolTip="Rechte horizontale streep (Shift wisselt)"
|
||||||
Checked="Straight_Changed" Unchecked="Straight_Changed">
|
Checked="Straight_Changed" Unchecked="Straight_Changed">
|
||||||
<Path Data="M2,9 H16 M2,5 V13 M16,5 V13" Stroke="{StaticResource InkBrush}" StrokeThickness="1.5"
|
<Path Data="M2,9 H16 M2,5 V13 M16,5 V13" Stroke="{DynamicResource InkBrush}" StrokeThickness="1.5"
|
||||||
StrokeStartLineCap="Round" StrokeEndLineCap="Round" Width="18" Height="18" />
|
StrokeStartLineCap="Round" StrokeEndLineCap="Round" Width="18" Height="18" />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -411,7 +431,7 @@
|
|||||||
<Grid Width="18" Height="18">
|
<Grid Width="18" Height="18">
|
||||||
<Rectangle Width="11" Height="11" RadiusX="2" RadiusY="2" Fill="#9A9A9A" Margin="5,5,0,0"
|
<Rectangle Width="11" Height="11" RadiusX="2" RadiusY="2" Fill="#9A9A9A" Margin="5,5,0,0"
|
||||||
HorizontalAlignment="Left" VerticalAlignment="Top" />
|
HorizontalAlignment="Left" VerticalAlignment="Top" />
|
||||||
<Rectangle Width="11" Height="11" RadiusX="2" RadiusY="2" Fill="White" Stroke="{StaticResource InkBrush}"
|
<Rectangle Width="11" Height="11" RadiusX="2" RadiusY="2" Fill="{DynamicResource ControlBrush}" Stroke="{DynamicResource InkBrush}"
|
||||||
StrokeThickness="1" Margin="2,2,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" />
|
StrokeThickness="1" Margin="2,2,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
@@ -422,9 +442,9 @@
|
|||||||
|
|
||||||
<!-- Canvas -->
|
<!-- Canvas -->
|
||||||
<ScrollViewer x:Name="Scroller" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
|
<ScrollViewer x:Name="Scroller" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
|
||||||
Background="#E9E9E9" PreviewMouseWheel="Scroller_PreviewMouseWheel" Focusable="False">
|
Background="{DynamicResource CanvasBrush}" PreviewMouseWheel="Scroller_PreviewMouseWheel" Focusable="False">
|
||||||
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24">
|
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24">
|
||||||
<Border BorderBrush="#C8C8C8" BorderThickness="1" Margin="-1" SnapsToDevicePixels="True" />
|
<Border BorderBrush="{DynamicResource CanvasBorderBrush}" BorderThickness="1" Margin="-1" SnapsToDevicePixels="True" />
|
||||||
<Grid x:Name="CanvasHost">
|
<Grid x:Name="CanvasHost">
|
||||||
<Grid.LayoutTransform>
|
<Grid.LayoutTransform>
|
||||||
<ScaleTransform x:Name="ZoomTransform" />
|
<ScaleTransform x:Name="ZoomTransform" />
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ public partial class EditorWindow : Window
|
|||||||
{
|
{
|
||||||
private enum DragMode { None, Creating, Moving, Handle, Cropping }
|
private enum DragMode { None, Creating, Moving, Handle, Cropping }
|
||||||
|
|
||||||
|
private enum ZOrder { Front, Forward, Backward, Back }
|
||||||
|
|
||||||
private static readonly double[] ThicknessValues = { 0, 1, 2, 3, 4, 6, 8, 12, 16, 20, 24, 32 };
|
private static readonly double[] ThicknessValues = { 0, 1, 2, 3, 4, 6, 8, 12, 16, 20, 24, 32 };
|
||||||
private static readonly double[] FontSizeValues = { 10, 12, 14, 16, 18, 20, 24, 28, 32, 40, 48, 64 };
|
private static readonly double[] FontSizeValues = { 10, 12, 14, 16, 18, 20, 24, 28, 32, 40, 48, 64 };
|
||||||
private static readonly double[] SizeValues = { 16, 20, 24, 28, 32, 40, 48, 56, 64, 80 };
|
private static readonly double[] SizeValues = { 16, 20, 24, 28, 32, 40, 48, 56, 64, 80 };
|
||||||
@@ -33,11 +35,31 @@ public partial class EditorWindow : Window
|
|||||||
private static readonly CursorKind[] CursorKindValues = { CursorKind.Arrow, CursorKind.Click, CursorKind.Hand };
|
private static readonly CursorKind[] CursorKindValues = { CursorKind.Arrow, CursorKind.Click, CursorKind.Hand };
|
||||||
private static readonly BlurMode[] BlurModeValues = { BlurMode.Pixelate, BlurMode.Blur };
|
private static readonly BlurMode[] BlurModeValues = { BlurMode.Pixelate, BlurMode.Blur };
|
||||||
|
|
||||||
|
/// <summary>Standaardvolgorde in de gesplitste indeling: eerst de bovenste werkbalk, dan de onderste.</summary>
|
||||||
|
private static readonly string[] DefaultSplitOrder =
|
||||||
|
{
|
||||||
|
"SplitSpacer1", "Select", "Sep1", "Rectangle", "Ellipse", "Line", "Arrow", "Text", "Step", "Cursor", "Emoji", "Highlight", "Blur",
|
||||||
|
"Sep2", "Crop", "Sep3", "Undo", "Redo", "Delete", "Sep4", "Clear", "SplitSpacer2", "Settings",
|
||||||
|
"New", "Screencast", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "ZoomOut", "Zoom", "ZoomIn"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Items die in de gesplitste indeling op de onderste werkbalk staan.</summary>
|
||||||
|
private static readonly HashSet<string> SplitBottomIds = new()
|
||||||
|
{
|
||||||
|
"New", "Screencast", "Spacer", "Save", "Upload", "Copy", "Sep5", "Spacer2", "ZoomOut", "Zoom", "ZoomIn"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Items die alleen in de gesplitste indeling op een werkbalk staan.</summary>
|
||||||
|
private static readonly HashSet<string> SplitOnlyIds = new() { "SplitSpacer1", "SplitSpacer2" };
|
||||||
|
|
||||||
private readonly EditorDocument _doc;
|
private readonly EditorDocument _doc;
|
||||||
private readonly UndoStack _undo = new();
|
private readonly UndoStack _undo = new();
|
||||||
private readonly AppSettings _settings;
|
private readonly AppSettings _settings;
|
||||||
private readonly DispatcherTimer _hintResetTimer = new() { Interval = TimeSpan.FromSeconds(3) };
|
private readonly DispatcherTimer _hintResetTimer = new() { Interval = TimeSpan.FromSeconds(3) };
|
||||||
private readonly List<string> _defaultToolbarOrder = new();
|
private readonly List<string> _defaultToolbarOrder = new();
|
||||||
|
private readonly List<string> _defaultSplitOrder = new();
|
||||||
|
private readonly Dictionary<string, UIElement> _toolbarItems = new();
|
||||||
|
private EditorLayout _appliedLayout;
|
||||||
|
|
||||||
private ToolKind _tool;
|
private ToolKind _tool;
|
||||||
private Annotation? _selected;
|
private Annotation? _selected;
|
||||||
@@ -68,12 +90,14 @@ public partial class EditorWindow : Window
|
|||||||
private bool _dirty;
|
private bool _dirty;
|
||||||
private bool _discarding;
|
private bool _discarding;
|
||||||
private bool _exported;
|
private bool _exported;
|
||||||
|
private bool _toolbarsHidden;
|
||||||
private readonly string? _windowName;
|
private readonly string? _windowName;
|
||||||
|
|
||||||
/// <param name="windowName">Programmanaam van het geknipte venster, voor %V in het naamsjabloon.</param>
|
/// <param name="windowName">Programmanaam van het geknipte venster, voor %V in het naamsjabloon.</param>
|
||||||
public EditorWindow(BitmapSource image, AppSettings settings, bool isBlank = false, string? windowName = null)
|
public EditorWindow(BitmapSource image, AppSettings settings, bool isBlank = false, string? windowName = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
ThemeManager.Attach(this);
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_windowName = windowName;
|
_windowName = windowName;
|
||||||
_doc = new EditorDocument(image);
|
_doc = new EditorDocument(image);
|
||||||
@@ -117,6 +141,10 @@ public partial class EditorWindow : Window
|
|||||||
DpiChanged += (_, _) => ApplyZoom();
|
DpiChanged += (_, _) => ApplyZoom();
|
||||||
PreviewKeyDown += OnPreviewKeyDown;
|
PreviewKeyDown += OnPreviewKeyDown;
|
||||||
|
|
||||||
|
App.Instance.Updates.AvailabilityChanged += OnUpdateAvailabilityChanged;
|
||||||
|
Closed += (_, _) => App.Instance.Updates.AvailabilityChanged -= OnUpdateAvailabilityChanged;
|
||||||
|
ShowUpdateBadge();
|
||||||
|
|
||||||
InitializeToolbar();
|
InitializeToolbar();
|
||||||
ApplySaveButton();
|
ApplySaveButton();
|
||||||
_suppressStyleEvents = false;
|
_suppressStyleEvents = false;
|
||||||
@@ -133,45 +161,73 @@ public partial class EditorWindow : Window
|
|||||||
|
|
||||||
private void InitializeToolbar()
|
private void InitializeToolbar()
|
||||||
{
|
{
|
||||||
_defaultToolbarOrder.AddRange(ToolbarItems.Items.Select(ItemId));
|
|
||||||
ApplyToolbarOrder(_settings.ToolbarOrder);
|
|
||||||
foreach (UIElement item in ToolbarItems.Items)
|
foreach (UIElement item in ToolbarItems.Items)
|
||||||
|
{
|
||||||
|
_toolbarItems[ItemId(item)] = item;
|
||||||
|
if (!SplitOnlyIds.Contains(ItemId(item)))
|
||||||
|
{
|
||||||
|
_defaultToolbarOrder.Add(ItemId(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_defaultSplitOrder.AddRange(DefaultSplitOrder.Where(_toolbarItems.ContainsKey));
|
||||||
|
_defaultSplitOrder.AddRange(_defaultToolbarOrder.Except(_defaultSplitOrder));
|
||||||
|
ApplyToolbarLayout();
|
||||||
|
foreach (UIElement item in _toolbarItems.Values)
|
||||||
{
|
{
|
||||||
item.Visibility = _settings.ToolbarHidden.Contains(ItemId(item)) ? Visibility.Collapsed : Visibility.Visible;
|
item.Visibility = _settings.ToolbarHidden.Contains(ItemId(item)) ? Visibility.Collapsed : Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyUploadButton();
|
ApplyUploadButton();
|
||||||
ApplyToolbarPosition();
|
|
||||||
ApplyToolbarLock();
|
ApplyToolbarLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ItemId(UIElement item) => ToolbarPanel.GetItemId(item) ?? string.Empty;
|
private static string ItemId(UIElement item) => ToolbarPanel.GetItemId(item) ?? string.Empty;
|
||||||
|
|
||||||
private void ApplyToolbarOrder(IReadOnlyList<string> saved)
|
private bool IsSplitLayout => _settings.EditorLayout == EditorLayout.Split;
|
||||||
{
|
|
||||||
Dictionary<string, UIElement> byId = ToolbarItems.Items.ToDictionary(ItemId);
|
|
||||||
List<string> order = saved.Where(byId.ContainsKey).Distinct().ToList();
|
|
||||||
|
|
||||||
// Items die nog niet in de opgeslagen volgorde staan (zoals nieuwe knoppen) komen na hun standaardvoorganger.
|
private IEnumerable<UIElement> AllToolbarItems => ToolbarItems.Items.Concat(ActionItems.Items);
|
||||||
for (int i = 0; i < _defaultToolbarOrder.Count; i++)
|
|
||||||
|
/// <summary>Items volgens de opgeslagen volgorde van de huidige indeling over de bovenste en onderste werkbalk verdelen.</summary>
|
||||||
|
private void ApplyToolbarLayout()
|
||||||
{
|
{
|
||||||
string id = _defaultToolbarOrder[i];
|
bool split = IsSplitLayout;
|
||||||
if (!order.Contains(id))
|
_appliedLayout = _settings.EditorLayout;
|
||||||
{
|
List<string> order = split
|
||||||
order.Insert(i == 0 ? 0 : order.IndexOf(_defaultToolbarOrder[i - 1]) + 1, id);
|
? MergeToolbarOrder(_settings.SplitToolbarOrder, _defaultSplitOrder)
|
||||||
}
|
: MergeToolbarOrder(_settings.ToolbarOrder, _defaultToolbarOrder);
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItems.Children.Clear();
|
ToolbarItems.Children.Clear();
|
||||||
foreach (string id in order)
|
ActionItems.Children.Clear();
|
||||||
|
foreach (string id in order.Where(id => split || !SplitOnlyIds.Contains(id)))
|
||||||
{
|
{
|
||||||
ToolbarItems.Children.Add(byId[id]);
|
(split && SplitBottomIds.Contains(id) ? ActionItems : ToolbarItems).Children.Add(_toolbarItems[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActionBar.Visibility = split && !_toolbarsHidden ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
ApplyToolbarPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> MergeToolbarOrder(IReadOnlyList<string> saved, IReadOnlyList<string> defaults)
|
||||||
|
{
|
||||||
|
List<string> order = saved.Where(_toolbarItems.ContainsKey).Distinct().ToList();
|
||||||
|
|
||||||
|
// Items die nog niet in de opgeslagen volgorde staan (zoals nieuwe knoppen) komen na hun standaardvoorganger.
|
||||||
|
for (int i = 0; i < defaults.Count; i++)
|
||||||
|
{
|
||||||
|
string id = defaults[i];
|
||||||
|
if (!order.Contains(id))
|
||||||
|
{
|
||||||
|
order.Insert(i == 0 ? 0 : order.IndexOf(defaults[i - 1]) + 1, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyToolbarPosition()
|
private void ApplyToolbarPosition()
|
||||||
{
|
{
|
||||||
Dock dock = _settings.ToolbarPosition;
|
Dock dock = IsSplitLayout ? Dock.Top : _settings.ToolbarPosition;
|
||||||
bool vertical = dock is Dock.Left or Dock.Right;
|
bool vertical = dock is Dock.Left or Dock.Right;
|
||||||
DockPanel.SetDock(Toolbar, dock);
|
DockPanel.SetDock(Toolbar, dock);
|
||||||
ToolbarItems.Orientation = vertical ? Orientation.Vertical : Orientation.Horizontal;
|
ToolbarItems.Orientation = vertical ? Orientation.Vertical : Orientation.Horizontal;
|
||||||
@@ -189,8 +245,11 @@ public partial class EditorWindow : Window
|
|||||||
private void ApplyToolbarLock()
|
private void ApplyToolbarLock()
|
||||||
{
|
{
|
||||||
bool locked = _settings.ToolbarLocked;
|
bool locked = _settings.ToolbarLocked;
|
||||||
|
string background = locked ? "ToolbarBrush" : "UnlockedToolbarBrush";
|
||||||
ToolbarItems.IsLocked = locked;
|
ToolbarItems.IsLocked = locked;
|
||||||
Toolbar.Background = locked ? (Brush)FindResource("ToolbarBrush") : new SolidColorBrush(Color.FromRgb(0xEA, 0xF2, 0xFB));
|
ActionItems.IsLocked = locked;
|
||||||
|
Toolbar.SetResourceReference(Border.BackgroundProperty, background);
|
||||||
|
ActionBar.SetResourceReference(Border.BackgroundProperty, background);
|
||||||
UpdateHint();
|
UpdateHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,53 +261,112 @@ public partial class EditorWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels alleen tonen als alles op één regel past; staat de werkbalk opzij, dan altijd alleen iconen.
|
|
||||||
private void UpdateToolbarLayout()
|
private void UpdateToolbarLayout()
|
||||||
{
|
{
|
||||||
if (ToolbarItems.Orientation == Orientation.Vertical)
|
UpdateActionLabels(Toolbar, ToolbarItems);
|
||||||
|
UpdateActionLabels(ActionBar, ActionItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels alleen tonen als alles op één regel past; staat de werkbalk opzij, dan altijd alleen iconen.
|
||||||
|
private void UpdateActionLabels(Border bar, ToolbarPanel panel)
|
||||||
|
{
|
||||||
|
List<TextBlock> labels = new[] { NewLabel, ClearLabel, ScreencastLabel, SaveLabel, UploadLabel, CopyLabel }
|
||||||
|
.Where(label => PanelOf(label) == panel).ToList();
|
||||||
|
if (labels.Count == 0)
|
||||||
{
|
{
|
||||||
SetActionLabelsVisible(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double available = Toolbar.ActualWidth - Toolbar.Padding.Left - Toolbar.Padding.Right
|
if (panel.Orientation == Orientation.Vertical)
|
||||||
- Toolbar.BorderThickness.Left - Toolbar.BorderThickness.Right;
|
{
|
||||||
SetActionLabelsVisible(true);
|
SetActionLabelsVisible(panel, labels, false);
|
||||||
ToolbarItems.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
return;
|
||||||
SetActionLabelsVisible(ToolbarItems.SingleLineLength <= available);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetActionLabelsVisible(bool visible)
|
double available = bar.ActualWidth - bar.Padding.Left - bar.Padding.Right
|
||||||
|
- bar.BorderThickness.Left - bar.BorderThickness.Right;
|
||||||
|
SetActionLabelsVisible(panel, labels, true);
|
||||||
|
panel.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||||
|
SetActionLabelsVisible(panel, labels, panel.SingleLineLength <= available);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ToolbarPanel? PanelOf(DependencyObject element)
|
||||||
|
{
|
||||||
|
for (DependencyObject? d = element; d != null; d = VisualTreeHelper.GetParent(d))
|
||||||
|
{
|
||||||
|
if (d is ToolbarPanel panel)
|
||||||
|
{
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetActionLabelsVisible(ToolbarPanel panel, IEnumerable<TextBlock> labels, bool visible)
|
||||||
{
|
{
|
||||||
Visibility visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
Visibility visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
||||||
foreach (TextBlock label in new[] { NewLabel, ClearLabel, ScreencastLabel, SaveLabel, UploadLabel, CopyLabel })
|
foreach (TextBlock label in labels)
|
||||||
{
|
{
|
||||||
label.Visibility = visibility;
|
label.Visibility = visibility;
|
||||||
|
|
||||||
// Anders geeft een directe Measure() van het paneel nog de gecachete knopbreedtes terug.
|
// Anders geeft een directe Measure() van het paneel nog de gecachete knopbreedtes terug.
|
||||||
for (DependencyObject? d = label; d != null && d != ToolbarItems; d = VisualTreeHelper.GetParent(d))
|
for (DependencyObject? d = label; d != null && d != panel; d = VisualTreeHelper.GetParent(d))
|
||||||
{
|
{
|
||||||
(d as UIElement)?.InvalidateMeasure();
|
(d as UIElement)?.InvalidateMeasure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItems.InvalidateMeasure();
|
panel.InvalidateMeasure();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToolbarItems_OrderChanged(object? sender, EventArgs e)
|
private void ToolbarItems_OrderChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (IsSplitLayout)
|
||||||
|
{
|
||||||
|
_settings.SplitToolbarOrder = AllToolbarItems.Select(ItemId).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
_settings.ToolbarOrder = ToolbarItems.Items.Select(ItemId).ToList();
|
_settings.ToolbarOrder = ToolbarItems.Items.Select(ItemId).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
UpdateToolbarLayout();
|
UpdateToolbarLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetEditorLayout(EditorLayout layout)
|
||||||
|
{
|
||||||
|
_settings.EditorLayout = layout;
|
||||||
|
ApplyToolbarLayout();
|
||||||
|
}
|
||||||
|
|
||||||
private void Toolbar_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
private void Toolbar_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||||
{
|
{
|
||||||
ToolbarMenu.Items.Clear();
|
var bar = (Border)sender;
|
||||||
|
ToolbarPanel panel = bar == ActionBar ? ActionItems : ToolbarItems;
|
||||||
|
ContextMenu menu = bar.ContextMenu;
|
||||||
|
menu.Items.Clear();
|
||||||
|
|
||||||
var lockItem = new MenuItem { Header = "Werkbalk vergrendelen", IsCheckable = true, IsChecked = _settings.ToolbarLocked };
|
var lockItem = new MenuItem { Header = "Werkbalk vergrendelen", IsCheckable = true, IsChecked = _settings.ToolbarLocked };
|
||||||
lockItem.Click += (_, _) => SetToolbarLocked(lockItem.IsChecked);
|
lockItem.Click += (_, _) => SetToolbarLocked(lockItem.IsChecked);
|
||||||
ToolbarMenu.Items.Add(lockItem);
|
menu.Items.Add(lockItem);
|
||||||
|
|
||||||
|
var layout = new MenuItem { Header = "Indeling" };
|
||||||
|
foreach ((EditorLayout value, string label) in new[]
|
||||||
|
{
|
||||||
|
(EditorLayout.Split, "Split-weergave: gereedschappen boven, opslaan onder"),
|
||||||
|
(EditorLayout.Single, "Eén werkbalk")
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var item = new MenuItem { Header = label, IsCheckable = true, IsChecked = _settings.EditorLayout == value };
|
||||||
|
item.Click += (_, _) => SetEditorLayout(value);
|
||||||
|
layout.Items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.Items.Add(layout);
|
||||||
|
|
||||||
|
if (!IsSplitLayout)
|
||||||
|
{
|
||||||
var position = new MenuItem { Header = "Positie" };
|
var position = new MenuItem { Header = "Positie" };
|
||||||
foreach ((Dock dock, string label) in new[] { (Dock.Top, "Boven"), (Dock.Bottom, "Onder"), (Dock.Left, "Links"), (Dock.Right, "Rechts") })
|
foreach ((Dock dock, string label) in new[] { (Dock.Top, "Boven"), (Dock.Bottom, "Onder"), (Dock.Left, "Links"), (Dock.Right, "Rechts") })
|
||||||
{
|
{
|
||||||
@@ -261,14 +379,18 @@ public partial class EditorWindow : Window
|
|||||||
position.Items.Add(item);
|
position.Items.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarMenu.Items.Add(position);
|
menu.Items.Add(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panel.Items.Any(i => i is ToolbarSpacer))
|
||||||
|
{
|
||||||
var swap = new MenuItem { Header = "Groepen omwisselen" };
|
var swap = new MenuItem { Header = "Groepen omwisselen" };
|
||||||
swap.Click += (_, _) => SwapToolbarGroups();
|
swap.Click += (_, _) => SwapToolbarGroups(panel);
|
||||||
ToolbarMenu.Items.Add(swap);
|
menu.Items.Add(swap);
|
||||||
|
}
|
||||||
|
|
||||||
var buttons = new MenuItem { Header = "Knoppen tonen" };
|
var buttons = new MenuItem { Header = "Knoppen tonen" };
|
||||||
foreach (UIElement element in ToolbarItems.Items)
|
foreach (UIElement element in AllToolbarItems)
|
||||||
{
|
{
|
||||||
if (ToolbarPanel.GetTitle(element) is not { } title || (element == UploadButton && !CanUpload))
|
if (ToolbarPanel.GetTitle(element) is not { } title || (element == UploadButton && !CanUpload))
|
||||||
{
|
{
|
||||||
@@ -280,12 +402,12 @@ public partial class EditorWindow : Window
|
|||||||
buttons.Items.Add(item);
|
buttons.Items.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarMenu.Items.Add(buttons);
|
menu.Items.Add(buttons);
|
||||||
ToolbarMenu.Items.Add(new Separator());
|
menu.Items.Add(new Separator());
|
||||||
|
|
||||||
var reset = new MenuItem { Header = "Standaard herstellen" };
|
var reset = new MenuItem { Header = "Standaard herstellen" };
|
||||||
reset.Click += (_, _) => ResetToolbar();
|
reset.Click += (_, _) => ResetToolbar();
|
||||||
ToolbarMenu.Items.Add(reset);
|
menu.Items.Add(reset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetToolbarLocked(bool locked)
|
private void SetToolbarLocked(bool locked)
|
||||||
@@ -308,49 +430,50 @@ public partial class EditorWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>De groepen tussen de flexibele ruimtes in omgekeerde volgorde zetten; een middengroep blijft in het midden.</summary>
|
/// <summary>De groepen tussen de flexibele ruimtes in omgekeerde volgorde zetten; een middengroep blijft in het midden.</summary>
|
||||||
private void SwapToolbarGroups()
|
private void SwapToolbarGroups(ToolbarPanel panel)
|
||||||
{
|
{
|
||||||
var groups = new List<List<string>> { new() };
|
var groups = new List<List<UIElement>> { new() };
|
||||||
var spacers = new List<string>();
|
var spacers = new List<UIElement>();
|
||||||
foreach (UIElement item in ToolbarItems.Items)
|
foreach (UIElement item in panel.Items)
|
||||||
{
|
{
|
||||||
if (item is ToolbarSpacer)
|
if (item is ToolbarSpacer)
|
||||||
{
|
{
|
||||||
spacers.Add(ItemId(item));
|
spacers.Add(item);
|
||||||
groups.Add(new List<string>());
|
groups.Add(new List<UIElement>());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
groups[^1].Add(ItemId(item));
|
groups[^1].Add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.Reverse();
|
groups.Reverse();
|
||||||
var order = new List<string>(groups[0]);
|
var order = new List<UIElement>(groups[0]);
|
||||||
for (int i = 0; i < spacers.Count; i++)
|
for (int i = 0; i < spacers.Count; i++)
|
||||||
{
|
{
|
||||||
order.Add(spacers[i]);
|
order.Add(spacers[i]);
|
||||||
order.AddRange(groups[i + 1]);
|
order.AddRange(groups[i + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyToolbarOrder(order);
|
panel.Children.Clear();
|
||||||
|
order.ForEach(item => panel.Children.Add(item));
|
||||||
ToolbarItems_OrderChanged(this, EventArgs.Empty);
|
ToolbarItems_OrderChanged(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetToolbar()
|
private void ResetToolbar()
|
||||||
{
|
{
|
||||||
_settings.ToolbarOrder = new List<string>();
|
_settings.ToolbarOrder = new List<string>();
|
||||||
|
_settings.SplitToolbarOrder = new List<string>();
|
||||||
_settings.ToolbarHidden = new List<string>();
|
_settings.ToolbarHidden = new List<string>();
|
||||||
_settings.ToolbarPosition = Dock.Top;
|
_settings.ToolbarPosition = Dock.Top;
|
||||||
_settings.ToolbarLocked = true;
|
_settings.ToolbarLocked = true;
|
||||||
ApplyToolbarOrder(_defaultToolbarOrder);
|
ApplyToolbarLayout();
|
||||||
foreach (UIElement item in ToolbarItems.Items)
|
foreach (UIElement item in _toolbarItems.Values)
|
||||||
{
|
{
|
||||||
item.Visibility = Visibility.Visible;
|
item.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyUploadButton();
|
ApplyUploadButton();
|
||||||
ApplyToolbarPosition();
|
|
||||||
ApplyToolbarLock();
|
ApplyToolbarLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +481,7 @@ public partial class EditorWindow : Window
|
|||||||
|
|
||||||
private void SelectTool(ToolKind tool)
|
private void SelectTool(ToolKind tool)
|
||||||
{
|
{
|
||||||
foreach (RadioButton button in ToolbarItems.Items.OfType<RadioButton>())
|
foreach (RadioButton button in _toolbarItems.Values.OfType<RadioButton>())
|
||||||
{
|
{
|
||||||
if ((string)button.Tag == tool.ToString())
|
if ((string)button.Tag == tool.ToString())
|
||||||
{
|
{
|
||||||
@@ -452,6 +575,14 @@ public partial class EditorWindow : Window
|
|||||||
if (_selected != null)
|
if (_selected != null)
|
||||||
{
|
{
|
||||||
int handle = Surface.HitHandle(_selected, p);
|
int handle = Surface.HitHandle(_selected, p);
|
||||||
|
if (handle >= 0 && e.ClickCount == 2 && _selected.RemoveHandle(handle))
|
||||||
|
{
|
||||||
|
Commit(_pending);
|
||||||
|
_pending = null;
|
||||||
|
Surface.InvalidateVisual();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (handle >= 0)
|
if (handle >= 0)
|
||||||
{
|
{
|
||||||
_handleIndex = handle;
|
_handleIndex = handle;
|
||||||
@@ -723,26 +854,53 @@ public partial class EditorWindow : Window
|
|||||||
private void Surface_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
private void Surface_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
CommitTextEdit();
|
CommitTextEdit();
|
||||||
Annotation? hit = HitTest(e.GetPosition(Surface));
|
Point point = e.GetPosition(Surface);
|
||||||
if (hit == null)
|
Annotation? hit = HitTest(point);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetSelected(hit);
|
SetSelected(hit);
|
||||||
|
bool has = hit != null;
|
||||||
|
int index = hit != null ? _doc.Items.IndexOf(hit) : -1;
|
||||||
|
int last = _doc.Items.Count - 1;
|
||||||
|
|
||||||
var menu = new ContextMenu { PlacementTarget = Surface };
|
var menu = new ContextMenu { PlacementTarget = Surface };
|
||||||
menu.Items.Add(MenuItem("Naar voren", () => Reorder(hit, toFront: true)));
|
menu.Items.Add(MenuItem("Ongedaan maken", "\uE7A7", "Ctrl+Z", Undo, _undo.CanUndo));
|
||||||
menu.Items.Add(MenuItem("Naar achteren", () => Reorder(hit, toFront: false)));
|
menu.Items.Add(MenuItem("Opnieuw", "\uE7A6", "Ctrl+Y", Redo, _undo.CanRedo));
|
||||||
menu.Items.Add(MenuItem("Dupliceren", () => Duplicate(hit)));
|
|
||||||
menu.Items.Add(new Separator());
|
menu.Items.Add(new Separator());
|
||||||
menu.Items.Add(MenuItem("Verwijderen", DeleteSelected));
|
menu.Items.Add(MenuItem("Verwijder", "\uE74D", "Delete", DeleteSelected, has));
|
||||||
|
menu.Items.Add(MenuItem("Verwijder alles", "\uE894", "Shift+Delete", DeleteAll, last >= 0));
|
||||||
|
menu.Items.Add(MenuItem("Samenvoegen", "\uE81E", "Ctrl+Shift+F", Flatten, last >= 0));
|
||||||
|
menu.Items.Add(new Separator());
|
||||||
|
menu.Items.Add(MenuItem("Knip", "\uE8C6", "Ctrl+X", () => CutAnnotation(hit!), has));
|
||||||
|
menu.Items.Add(MenuItem("Kopieer", "\uE8C8", "Ctrl+Shift+C", () => CopyAnnotation(hit!), has));
|
||||||
|
menu.Items.Add(MenuItem("Plak", "\uE77F", "Ctrl+V", () => Paste(point), CanPaste()));
|
||||||
|
menu.Items.Add(MenuItem("Dupliceren", "\uE710", "Ctrl+D", () => Duplicate(hit!), has));
|
||||||
|
menu.Items.Add(new Separator());
|
||||||
|
menu.Items.Add(MenuItem("Naar voorgrond", "\uE898", "Home", () => Reorder(hit!, ZOrder.Front), has && index < last));
|
||||||
|
menu.Items.Add(MenuItem("Naar voren", "\uE74A", "PageUp", () => Reorder(hit!, ZOrder.Forward), has && index < last));
|
||||||
|
menu.Items.Add(MenuItem("Naar achteren", "\uE74B", "PageDown", () => Reorder(hit!, ZOrder.Backward), has && index > 0));
|
||||||
|
menu.Items.Add(MenuItem("Naar achtergrond", "\uE896", "End", () => Reorder(hit!, ZOrder.Back), has && index > 0));
|
||||||
|
menu.Items.Add(new Separator());
|
||||||
|
menu.Items.Add(_toolbarsHidden
|
||||||
|
? MenuItem("Werkbalken tonen", "\uE7B3", "Ctrl+H", ToggleToolbars)
|
||||||
|
: MenuItem("Werkbalken verbergen", "\uED1A", "Ctrl+H", ToggleToolbars));
|
||||||
menu.IsOpen = true;
|
menu.IsOpen = true;
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MenuItem MenuItem(string header, Action action)
|
private static MenuItem MenuItem(string header, string glyph, string gesture, Action action, bool isEnabled = true)
|
||||||
{
|
{
|
||||||
var item = new MenuItem { Header = header };
|
var item = new MenuItem
|
||||||
|
{
|
||||||
|
Header = header,
|
||||||
|
InputGestureText = gesture,
|
||||||
|
IsEnabled = isEnabled,
|
||||||
|
Icon = new TextBlock
|
||||||
|
{
|
||||||
|
Text = glyph,
|
||||||
|
FontFamily = (FontFamily)Application.Current.FindResource("IconFont"),
|
||||||
|
FontSize = 14,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
}
|
||||||
|
};
|
||||||
item.Click += (_, _) => action();
|
item.Click += (_, _) => action();
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
@@ -781,20 +939,122 @@ public partial class EditorWindow : Window
|
|||||||
SetSelected(null);
|
SetSelected(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Reorder(Annotation item, bool toFront)
|
private void DeleteAll()
|
||||||
{
|
{
|
||||||
|
if (_doc.Items.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Commit(_doc.CaptureState());
|
Commit(_doc.CaptureState());
|
||||||
_doc.Items.Remove(item);
|
_doc.Items.Clear();
|
||||||
if (toFront)
|
SetSelected(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Flatten()
|
||||||
{
|
{
|
||||||
_doc.Items.Add(item);
|
CommitTextEdit();
|
||||||
|
if (_doc.Items.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Commit(_doc.CaptureState());
|
||||||
|
_doc.Flatten(DocumentRenderer.Export(_doc));
|
||||||
|
SetSelected(null);
|
||||||
|
ShowTemporaryHint("Annotaties samengevoegd met de afbeelding.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reorder(Annotation item, ZOrder order)
|
||||||
|
{
|
||||||
|
int index = _doc.Items.IndexOf(item);
|
||||||
|
int last = _doc.Items.Count - 1;
|
||||||
|
int target = order switch
|
||||||
|
{
|
||||||
|
ZOrder.Front => last,
|
||||||
|
ZOrder.Forward => Math.Min(index + 1, last),
|
||||||
|
ZOrder.Backward => Math.Max(index - 1, 0),
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
if (index < 0 || target == index)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Commit(_doc.CaptureState());
|
||||||
|
_doc.Items.RemoveAt(index);
|
||||||
|
_doc.Items.Insert(target, item);
|
||||||
|
Surface.InvalidateVisual();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyAnnotation(Annotation item)
|
||||||
|
{
|
||||||
|
if (!AnnotationClipboard.Set(item))
|
||||||
|
{
|
||||||
|
ShowTemporaryHint("Kopiëren mislukt: het klembord is bezet. Probeer het opnieuw.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CutAnnotation(Annotation item)
|
||||||
|
{
|
||||||
|
if (AnnotationClipboard.Set(item))
|
||||||
|
{
|
||||||
|
SetSelected(item);
|
||||||
|
DeleteSelected();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_doc.Items.Insert(0, item);
|
ShowTemporaryHint("Knippen mislukt: het klembord is bezet. Probeer het opnieuw.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Surface.InvalidateVisual();
|
/// <param name="at">Middelpunt voor de geplakte annotatie; zonder punt een kleine verschuiving.</param>
|
||||||
|
private void Paste(Point? at = null)
|
||||||
|
{
|
||||||
|
if (AnnotationClipboard.Get() is not { } copy)
|
||||||
|
{
|
||||||
|
PasteImage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Commit(_doc.CaptureState());
|
||||||
|
Rect bounds = copy.Bounds;
|
||||||
|
Vector offset = at is { } point
|
||||||
|
? point - new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2)
|
||||||
|
: new Vector(12 / Surface.ViewScale, 12 / Surface.ViewScale);
|
||||||
|
copy.Move(offset);
|
||||||
|
_doc.Items.Add(copy);
|
||||||
|
SetSelected(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanPaste()
|
||||||
|
{
|
||||||
|
if (AnnotationClipboard.Get() != null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Clipboard.ContainsImage() || Clipboard.ContainsData("PNG") || Clipboard.ContainsFileDropList();
|
||||||
|
}
|
||||||
|
catch (System.Runtime.InteropServices.ExternalException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleToolbars()
|
||||||
|
{
|
||||||
|
_toolbarsHidden = !_toolbarsHidden;
|
||||||
|
Visibility visibility = _toolbarsHidden ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
Toolbar.Visibility = visibility;
|
||||||
|
PropsBar.Visibility = visibility;
|
||||||
|
ActionBar.Visibility = IsSplitLayout ? visibility : Visibility.Collapsed;
|
||||||
|
if (_toolbarsHidden)
|
||||||
|
{
|
||||||
|
ShowTemporaryHint("Werkbalken verborgen. Ctrl+H of rechtsklik op het canvas om ze terug te halen.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Duplicate(Annotation item)
|
private void Duplicate(Annotation item)
|
||||||
@@ -1365,6 +1625,11 @@ public partial class EditorWindow : Window
|
|||||||
protected override void OnActivated(EventArgs e)
|
protected override void OnActivated(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnActivated(e);
|
base.OnActivated(e);
|
||||||
|
if (_appliedLayout != _settings.EditorLayout)
|
||||||
|
{
|
||||||
|
ApplyToolbarLayout();
|
||||||
|
}
|
||||||
|
|
||||||
ApplySaveButton();
|
ApplySaveButton();
|
||||||
ApplyUploadButton();
|
ApplyUploadButton();
|
||||||
}
|
}
|
||||||
@@ -1398,6 +1663,7 @@ public partial class EditorWindow : Window
|
|||||||
{
|
{
|
||||||
using System.IO.MemoryStream png = ImageExporter.EncodePng(image);
|
using System.IO.MemoryStream png = ImageExporter.EncodePng(image);
|
||||||
string url = await ZiplineUploader.UploadAsync(config, png, $"Knipsel {DateTime.Now:yyyy-MM-dd HHmmss}.png", "image/png");
|
string url = await ZiplineUploader.UploadAsync(config, png, $"Knipsel {DateTime.Now:yyyy-MM-dd HHmmss}.png", "image/png");
|
||||||
|
Kniip.History.HistoryStore.Add(Kniip.History.HistoryKind.Snip, null, url, image);
|
||||||
_dirty = false;
|
_dirty = false;
|
||||||
_exported = true;
|
_exported = true;
|
||||||
ShowTemporaryHint(ClipboardWriter.SetText(url)
|
ShowTemporaryHint(ClipboardWriter.SetText(url)
|
||||||
@@ -1494,6 +1760,7 @@ public partial class EditorWindow : Window
|
|||||||
App.Instance.Capture.IgnorePath(path);
|
App.Instance.Capture.IgnorePath(path);
|
||||||
BitmapSource image = DocumentRenderer.Export(_doc);
|
BitmapSource image = DocumentRenderer.Export(_doc);
|
||||||
ImageExporter.Save(image, path);
|
ImageExporter.Save(image, path);
|
||||||
|
Kniip.History.HistoryStore.Add(Kniip.History.HistoryKind.Snip, path, null, image);
|
||||||
_dirty = false;
|
_dirty = false;
|
||||||
_exported = true;
|
_exported = true;
|
||||||
string name = System.IO.Path.GetFileName(path);
|
string name = System.IO.Path.GetFileName(path);
|
||||||
@@ -1539,6 +1806,15 @@ public partial class EditorWindow : Window
|
|||||||
|
|
||||||
private void Settings_Click(object sender, RoutedEventArgs e) => App.Instance.ShowSettings();
|
private void Settings_Click(object sender, RoutedEventArgs e) => App.Instance.ShowSettings();
|
||||||
|
|
||||||
|
private void OnUpdateAvailabilityChanged(object? sender, EventArgs e) => ShowUpdateBadge();
|
||||||
|
|
||||||
|
private void ShowUpdateBadge()
|
||||||
|
{
|
||||||
|
bool available = App.Instance.Updates.IsUpdateAvailable;
|
||||||
|
SettingsBadge.Visibility = available ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
SettingsButton.ToolTip = available ? "Instellingen - nieuwe versie beschikbaar" : "Instellingen";
|
||||||
|
}
|
||||||
|
|
||||||
private void NewSnip_Click(object sender, RoutedEventArgs e) => DiscardAndStartNewSnip();
|
private void NewSnip_Click(object sender, RoutedEventArgs e) => DiscardAndStartNewSnip();
|
||||||
|
|
||||||
private void Clear_Click(object sender, RoutedEventArgs e) => DiscardAndStartBlank();
|
private void Clear_Click(object sender, RoutedEventArgs e) => DiscardAndStartBlank();
|
||||||
@@ -1595,6 +1871,36 @@ public partial class EditorWindow : Window
|
|||||||
ReplaceWith(image, "Wil je doorgaan en de afbeelding van het klembord openen?");
|
ReplaceWith(image, "Wil je doorgaan en de afbeelding van het klembord openen?");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Window_PreviewDragOver(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (!ClipboardImageReader.CanDrop(e.Data))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Effects = DragDropEffects.Copy;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Window_PreviewDrop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (!ClipboardImageReader.CanDrop(e.Data))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
BitmapSource? image = ClipboardImageReader.FromDrop(e.Data);
|
||||||
|
if (image == null)
|
||||||
|
{
|
||||||
|
ShowTemporaryHint("Deze afbeelding kon niet worden geopend.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas na het slepen vragen: een dialoog tijdens Drop blokkeert Verkenner.
|
||||||
|
Dispatcher.BeginInvoke(() => ReplaceWith(image, "Wil je doorgaan en deze afbeelding openen?"));
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenImageFile()
|
private void OpenImageFile()
|
||||||
{
|
{
|
||||||
CommitTextEdit();
|
CommitTextEdit();
|
||||||
@@ -1698,10 +2004,26 @@ public partial class EditorWindow : Window
|
|||||||
break;
|
break;
|
||||||
case Key.Z:
|
case Key.Z:
|
||||||
Undo();
|
Undo();
|
||||||
|
break;
|
||||||
|
case Key.C when shift:
|
||||||
|
if (_selected != null)
|
||||||
|
{
|
||||||
|
CopyAnnotation(_selected);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case Key.C:
|
case Key.C:
|
||||||
CopyToClipboard();
|
CopyToClipboard();
|
||||||
break;
|
break;
|
||||||
|
case Key.X when _selected != null:
|
||||||
|
CutAnnotation(_selected);
|
||||||
|
break;
|
||||||
|
case Key.F when shift:
|
||||||
|
Flatten();
|
||||||
|
break;
|
||||||
|
case Key.H:
|
||||||
|
ToggleToolbars();
|
||||||
|
break;
|
||||||
case Key.S when shift:
|
case Key.S when shift:
|
||||||
SaveAs();
|
SaveAs();
|
||||||
break;
|
break;
|
||||||
@@ -1718,7 +2040,7 @@ public partial class EditorWindow : Window
|
|||||||
DiscardAndStartBlank();
|
DiscardAndStartBlank();
|
||||||
break;
|
break;
|
||||||
case Key.V:
|
case Key.V:
|
||||||
PasteImage();
|
Paste();
|
||||||
break;
|
break;
|
||||||
case Key.O:
|
case Key.O:
|
||||||
OpenImageFile();
|
OpenImageFile();
|
||||||
@@ -1758,10 +2080,25 @@ public partial class EditorWindow : Window
|
|||||||
double step = shift ? 10 : 1;
|
double step = shift ? 10 : 1;
|
||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
|
case Key.Delete when shift:
|
||||||
|
DeleteAll();
|
||||||
|
break;
|
||||||
case Key.Delete:
|
case Key.Delete:
|
||||||
case Key.Back:
|
case Key.Back:
|
||||||
DeleteSelected();
|
DeleteSelected();
|
||||||
break;
|
break;
|
||||||
|
case Key.Home when _selected != null:
|
||||||
|
Reorder(_selected, ZOrder.Front);
|
||||||
|
break;
|
||||||
|
case Key.PageUp when _selected != null:
|
||||||
|
Reorder(_selected, ZOrder.Forward);
|
||||||
|
break;
|
||||||
|
case Key.PageDown when _selected != null:
|
||||||
|
Reorder(_selected, ZOrder.Backward);
|
||||||
|
break;
|
||||||
|
case Key.End when _selected != null:
|
||||||
|
Reorder(_selected, ZOrder.Back);
|
||||||
|
break;
|
||||||
case Key.Escape:
|
case Key.Escape:
|
||||||
if (_drag != DragMode.None)
|
if (_drag != DragMode.None)
|
||||||
{
|
{
|
||||||
@@ -1847,8 +2184,8 @@ public partial class EditorWindow : Window
|
|||||||
ToolKind.Select => "Klik om te selecteren, sleep om te verplaatsen. Dubbelklik op tekst om te bewerken. Pijltoetsen verschuiven.",
|
ToolKind.Select => "Klik om te selecteren, sleep om te verplaatsen. Dubbelklik op tekst om te bewerken. Pijltoetsen verschuiven.",
|
||||||
ToolKind.Rectangle => "Sleep om een rechthoek te tekenen. Shift = vierkant.",
|
ToolKind.Rectangle => "Sleep om een rechthoek te tekenen. Shift = vierkant.",
|
||||||
ToolKind.Ellipse => "Sleep om een ellips te tekenen. Shift = cirkel.",
|
ToolKind.Ellipse => "Sleep om een ellips te tekenen. Shift = cirkel.",
|
||||||
ToolKind.Line => "Sleep om een lijn te tekenen. Shift = hoeken van 45°.",
|
ToolKind.Line => "Sleep om een lijn te tekenen. Shift = hoeken van 45°. Sleep een middenpunt om te buigen; dubbelklik op een buigpunt om het te verwijderen.",
|
||||||
ToolKind.Arrow => "Sleep om een pijl te tekenen. Shift = hoeken van 45°.",
|
ToolKind.Arrow => "Sleep om een pijl te tekenen. Shift = hoeken van 45°. Sleep een middenpunt om te buigen; dubbelklik op een buigpunt om het te verwijderen.",
|
||||||
ToolKind.Text => "Klik om een tekstkader te plaatsen, of klik op bestaande tekst om die te bewerken.",
|
ToolKind.Text => "Klik om een tekstkader te plaatsen, of klik op bestaande tekst om die te bewerken.",
|
||||||
ToolKind.Step => "Klik om een genummerde stap te plaatsen. De nummering loopt automatisch door.",
|
ToolKind.Step => "Klik om een genummerde stap te plaatsen. De nummering loopt automatisch door.",
|
||||||
ToolKind.Cursor => "Klik om een cursor te plaatsen; de punt komt precies op de klikplek.",
|
ToolKind.Cursor => "Klik om een cursor te plaatsen; de punt komt precies op de klikplek.",
|
||||||
|
|||||||
@@ -5,20 +5,20 @@
|
|||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<Grid>
|
<Grid>
|
||||||
<ToggleButton x:Name="Toggle" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30"
|
<ToggleButton x:Name="Toggle" Style="{StaticResource FlatButton}" Padding="6,4" MinWidth="0" MinHeight="30"
|
||||||
BorderBrush="#E0E0E0" Background="White" ToolTip="Emoji kiezen">
|
BorderBrush="{DynamicResource ControlBorderBrush}" Background="{DynamicResource ControlBrush}" ToolTip="Emoji kiezen">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Image x:Name="Face" Width="20" Height="20" RenderOptions.BitmapScalingMode="HighQuality" />
|
<Image x:Name="Face" Width="20" Height="20" RenderOptions.BitmapScalingMode="HighQuality" />
|
||||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="9" Margin="5,0,1,0" Foreground="#5C5C5C" />
|
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="9" Margin="5,0,1,0" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<Popup x:Name="PopupPanel" PlacementTarget="{Binding ElementName=Toggle}" Placement="Bottom" StaysOpen="False"
|
<Popup x:Name="PopupPanel" PlacementTarget="{Binding ElementName=Toggle}" Placement="Bottom" StaysOpen="False"
|
||||||
AllowsTransparency="True" PopupAnimation="Fade" Opened="Popup_Opened"
|
AllowsTransparency="True" PopupAnimation="Fade" Opened="Popup_Opened"
|
||||||
IsOpen="{Binding IsChecked, ElementName=Toggle, Mode=TwoWay}">
|
IsOpen="{Binding IsChecked, ElementName=Toggle, Mode=TwoWay}">
|
||||||
<Border Background="White" BorderBrush="#D6D6D6" BorderThickness="1" CornerRadius="6" Padding="8" Margin="0,4,10,10"
|
<Border Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1" CornerRadius="6" Padding="8" Margin="0,4,10,10"
|
||||||
PreviewKeyDown="Popup_PreviewKeyDown">
|
PreviewKeyDown="Popup_PreviewKeyDown">
|
||||||
<Border.Resources>
|
<Border.Resources>
|
||||||
<DataTemplate DataType="{x:Type local:EmojiHeaderRow}">
|
<DataTemplate DataType="{x:Type local:EmojiHeaderRow}">
|
||||||
<TextBlock Text="{Binding Title}" FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource SubtleTextBrush}"
|
<TextBlock Text="{Binding Title}" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource SubtleTextBrush}"
|
||||||
Margin="4,8,0,4" />
|
Margin="4,8,0,4" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
<DataTemplate DataType="{x:Type local:EmojiCellRow}">
|
<DataTemplate DataType="{x:Type local:EmojiCellRow}">
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<!-- Zoeken -->
|
<!-- Zoeken -->
|
||||||
<Grid DockPanel.Dock="Top" Margin="0,0,0,6">
|
<Grid DockPanel.Dock="Top" Margin="0,0,0,6">
|
||||||
<TextBox x:Name="SearchBox" Height="30" Padding="28,0,6,0" VerticalContentAlignment="Center" FontSize="13"
|
<TextBox x:Name="SearchBox" Height="30" Padding="28,0,6,0" VerticalContentAlignment="Center" FontSize="13"
|
||||||
BorderBrush="#D6D6D6" TextChanged="SearchBox_TextChanged" />
|
TextChanged="SearchBox_TextChanged" />
|
||||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="12" Foreground="#8A8A8A"
|
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="12" Foreground="#8A8A8A"
|
||||||
HorizontalAlignment="Left" Margin="9,0,0,0" IsHitTestVisible="False" />
|
HorizontalAlignment="Left" Margin="9,0,0,0" IsHitTestVisible="False" />
|
||||||
<TextBlock x:Name="SearchPlaceholder" Text="Zoeken (Nederlands of Engels)" Foreground="#8A8A8A" FontSize="13"
|
<TextBlock x:Name="SearchPlaceholder" Text="Zoeken (Nederlands of Engels)" Foreground="#8A8A8A" FontSize="13"
|
||||||
@@ -57,14 +57,14 @@
|
|||||||
<StackPanel x:Name="TabPanel" DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,4" />
|
<StackPanel x:Name="TabPanel" DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,4" />
|
||||||
|
|
||||||
<!-- Voorbeeld en huidskleur -->
|
<!-- Voorbeeld en huidskleur -->
|
||||||
<Border DockPanel.Dock="Bottom" BorderBrush="{StaticResource DividerBrush}" BorderThickness="0,1,0,0" Padding="2,8,0,0"
|
<Border DockPanel.Dock="Bottom" BorderBrush="{DynamicResource DividerBrush}" BorderThickness="0,1,0,0" Padding="2,8,0,0"
|
||||||
Margin="0,4,0,0">
|
Margin="0,4,0,0">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<StackPanel x:Name="TonePanel" DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center" />
|
<StackPanel x:Name="TonePanel" DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center" />
|
||||||
<Image x:Name="PreviewImage" Width="36" Height="36" RenderOptions.BitmapScalingMode="HighQuality" />
|
<Image x:Name="PreviewImage" Width="36" Height="36" RenderOptions.BitmapScalingMode="HighQuality" />
|
||||||
<StackPanel VerticalAlignment="Center" Margin="8,0,6,0">
|
<StackPanel VerticalAlignment="Center" Margin="8,0,6,0">
|
||||||
<TextBlock x:Name="PreviewName" FontSize="13" TextTrimming="CharacterEllipsis" />
|
<TextBlock x:Name="PreviewName" FontSize="13" TextTrimming="CharacterEllipsis" />
|
||||||
<TextBlock x:Name="PreviewCode" FontSize="11" Foreground="{StaticResource SubtleTextBrush}"
|
<TextBlock x:Name="PreviewCode" FontSize="11" Foreground="{DynamicResource SubtleTextBrush}"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</ItemsControl.Template>
|
</ItemsControl.Template>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
<TextBlock x:Name="NoResultsText" Text="Geen emoji gevonden" Foreground="{StaticResource SubtleTextBrush}"
|
<TextBlock x:Name="NoResultsText" Text="Geen emoji gevonden" Foreground="{DynamicResource SubtleTextBrush}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ using System.IO;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using Kniip.Capture;
|
using Kniip.Capture;
|
||||||
|
using Kniip.Interop;
|
||||||
|
|
||||||
namespace Kniip.Editor;
|
namespace Kniip.Editor;
|
||||||
|
|
||||||
@@ -68,13 +70,103 @@ internal static class ClipboardWriter
|
|||||||
return TrySet(data);
|
return TrySet(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Alleen een eigen markering op het klembord, zonder afbeelding of tekst.</summary>
|
||||||
|
public static bool SetMarker(string format, string value)
|
||||||
|
{
|
||||||
|
var data = new DataObject();
|
||||||
|
data.SetData(format, value);
|
||||||
|
return TrySet(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tekst gaat rechtstreeks via Win32 op het klembord. De OLE-route van WPF moet het klembord
|
||||||
|
/// daarna nog leegspoelen, en dat mislukt zodra een klembordbeheerder het meteen opent.
|
||||||
|
/// </summary>
|
||||||
public static bool SetText(string text)
|
public static bool SetText(string text)
|
||||||
{
|
{
|
||||||
|
if (TrySetUnicodeText(text))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var data = new DataObject();
|
var data = new DataObject();
|
||||||
data.SetText(text);
|
data.SetText(text);
|
||||||
return TrySet(data);
|
return TrySet(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static HwndSource? _owner;
|
||||||
|
|
||||||
|
private static IntPtr OwnerHandle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// Een venster als eigenaar is verplicht: zonder eigenaar weigert SetClipboardData na EmptyClipboard.
|
||||||
|
_owner ??= new HwndSource(new HwndSourceParameters("KniipClipboardWriter")
|
||||||
|
{
|
||||||
|
ParentWindow = NativeMethods.HWND_MESSAGE,
|
||||||
|
WindowStyle = 0
|
||||||
|
});
|
||||||
|
return _owner.Handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TrySetUnicodeText(string text)
|
||||||
|
{
|
||||||
|
IntPtr owner = OwnerHandle;
|
||||||
|
for (int attempt = 0; attempt < 20; attempt++)
|
||||||
|
{
|
||||||
|
if (NativeMethods.OpenClipboard(owner))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return NativeMethods.EmptyClipboard() && PutUnicodeText(text);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
NativeMethods.CloseClipboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool PutUnicodeText(string text)
|
||||||
|
{
|
||||||
|
IntPtr handle = NativeMethods.GlobalAlloc(NativeMethods.GMEM_MOVEABLE, (UIntPtr)((text.Length + 1) * 2));
|
||||||
|
if (handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IntPtr target = NativeMethods.GlobalLock(handle);
|
||||||
|
if (target == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
NativeMethods.GlobalFree(handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Marshal.Copy(text.ToCharArray(), 0, target, text.Length);
|
||||||
|
Marshal.WriteInt16(target, text.Length * 2, 0);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
NativeMethods.GlobalUnlock(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NativeMethods.SetClipboardData(NativeMethods.CF_UNICODETEXT, handle) == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
NativeMethods.GlobalFree(handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TrySet(DataObject data)
|
private static bool TrySet(DataObject data)
|
||||||
{
|
{
|
||||||
for (int attempt = 0; attempt < 5; attempt++)
|
for (int attempt = 0; attempt < 5; attempt++)
|
||||||
|
|||||||
@@ -477,6 +477,14 @@ public sealed class ToolbarSeparator : FrameworkElement
|
|||||||
{
|
{
|
||||||
private const double Thickness = 13;
|
private const double Thickness = 13;
|
||||||
|
|
||||||
|
private static readonly DependencyProperty LineBrushProperty = DependencyProperty.Register(
|
||||||
|
nameof(LineBrush), typeof(Brush), typeof(ToolbarSeparator),
|
||||||
|
new FrameworkPropertyMetadata(Brushes.LightGray, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||||
|
|
||||||
|
public ToolbarSeparator() => SetResourceReference(LineBrushProperty, "DividerBrush");
|
||||||
|
|
||||||
|
private Brush LineBrush => (Brush)GetValue(LineBrushProperty);
|
||||||
|
|
||||||
private bool Vertical => VisualTreeHelper.GetParent(this) is ToolbarPanel { Orientation: Orientation.Vertical };
|
private bool Vertical => VisualTreeHelper.GetParent(this) is ToolbarPanel { Orientation: Orientation.Vertical };
|
||||||
|
|
||||||
protected override Size MeasureOverride(Size availableSize) =>
|
protected override Size MeasureOverride(Size availableSize) =>
|
||||||
@@ -489,7 +497,7 @@ public sealed class ToolbarSeparator : FrameworkElement
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Brush brush = TryFindResource("DividerBrush") as Brush ?? Brushes.LightGray;
|
Brush brush = LineBrush;
|
||||||
Rect line = Vertical
|
Rect line = Vertical
|
||||||
? new Rect(6, Math.Floor(RenderSize.Height / 2), Math.Max(0, RenderSize.Width - 12), 1)
|
? new Rect(6, Math.Floor(RenderSize.Height / 2), Math.Max(0, RenderSize.Width - 12), 1)
|
||||||
: new Rect(Math.Floor(RenderSize.Width / 2), 6, 1, Math.Max(0, RenderSize.Height - 12));
|
: new Rect(Math.Floor(RenderSize.Width / 2), 6, 1, Math.Max(0, RenderSize.Height - 12));
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using Kniip.Editor;
|
||||||
|
using Kniip.Infrastructure;
|
||||||
|
|
||||||
|
namespace Kniip.History;
|
||||||
|
|
||||||
|
/// <param name="Glyph">Teken uit Segoe Fluent Icons.</param>
|
||||||
|
public sealed record HistoryCommand(string Label, string Glyph, bool Enabled, Action Run);
|
||||||
|
|
||||||
|
/// <summary>Wat je met een item uit de geschiedenis kunt doen, gedeeld door het systeemvakmenu en het geschiedenisvenster.</summary>
|
||||||
|
public static class HistoryActions
|
||||||
|
{
|
||||||
|
/// <summary>De acties voor een item; null staat voor een scheidingslijn.</summary>
|
||||||
|
public static IReadOnlyList<HistoryCommand?> CommandsFor(HistoryEntry entry)
|
||||||
|
{
|
||||||
|
bool file = entry.FileExists;
|
||||||
|
bool url = entry.Url is { Length: > 0 };
|
||||||
|
bool image = file && entry.Kind == HistoryKind.Snip;
|
||||||
|
return new HistoryCommand?[]
|
||||||
|
{
|
||||||
|
new("Openen", "\uE8E5", file || url, () => Open(entry)),
|
||||||
|
new("Openen in de editor", "\uE70F", image, () => App.Instance.OpenFile(entry.FilePath!)),
|
||||||
|
new("Link openen", "\uE774", url, () => Launch(entry.Url!)),
|
||||||
|
null,
|
||||||
|
new("Link kopiëren", "\uE71B", url, () => Copy(entry.Url!)),
|
||||||
|
new("Afbeelding kopiëren", "\uE8C8", image, () => CopyImage(entry.FilePath!)),
|
||||||
|
new("Pad kopiëren", "\uE8C8", file, () => Copy(entry.FilePath!)),
|
||||||
|
new("Tonen in map", "\uE838", file, () => ShowInFolder(entry.FilePath!))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bestand in het standaardprogramma, anders de link in de browser.</summary>
|
||||||
|
public static void Open(HistoryEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.FileExists)
|
||||||
|
{
|
||||||
|
Launch(entry.FilePath!);
|
||||||
|
}
|
||||||
|
else if (entry.Url is { Length: > 0 } url)
|
||||||
|
{
|
||||||
|
Launch(url);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Het bestand bestaat niet meer:\n{entry.FilePath}", "kniip", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void OpenFolder(string folder)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(folder);
|
||||||
|
Launch(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ShowInFolder(string path) =>
|
||||||
|
Run(() => Process.Start("explorer.exe", $"/select,\"{path}\""));
|
||||||
|
|
||||||
|
private static void Launch(string target) =>
|
||||||
|
Run(() => Process.Start(new ProcessStartInfo(target) { UseShellExecute = true }));
|
||||||
|
|
||||||
|
private static void Copy(string text)
|
||||||
|
{
|
||||||
|
if (!ClipboardWriter.SetText(text))
|
||||||
|
{
|
||||||
|
ClipboardBusy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyImage(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!ClipboardWriter.SetImage(PixelUtil.LoadFile(path), path))
|
||||||
|
{
|
||||||
|
ClipboardBusy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Kan '{Path.GetFileName(path)}' niet kopiëren.\n\n{ex.Message}", "kniip", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClipboardBusy() =>
|
||||||
|
MessageBox.Show("Kopiëren is mislukt: het klembord is bezet. Probeer het opnieuw.", "kniip", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
private static void Run(Action action)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Openen is mislukt.\n\n{ex.Message}", "kniip", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Kniip.History;
|
||||||
|
|
||||||
|
public enum HistoryKind
|
||||||
|
{
|
||||||
|
Snip,
|
||||||
|
Mp4,
|
||||||
|
Gif
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class HistoryEntry
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||||
|
public DateTime Time { get; set; } = DateTime.Now;
|
||||||
|
public HistoryKind Kind { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Opgeslagen bestand; leeg als het alleen is geüpload.</summary>
|
||||||
|
public string? FilePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Link na het uploaden; leeg als het alleen is opgeslagen.</summary>
|
||||||
|
public string? Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Bestandsnaam van de miniatuur in <see cref="HistoryStore.ThumbnailFolder"/>.</summary>
|
||||||
|
public string? Thumbnail { get; set; }
|
||||||
|
|
||||||
|
public int Width { get; set; }
|
||||||
|
public int Height { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Name => FilePath is { Length: > 0 }
|
||||||
|
? Path.GetFileName(FilePath)
|
||||||
|
: Url is { Length: > 0 } ? Url[(Url.TrimEnd('/').LastIndexOf('/') + 1)..] : "";
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string KindText => Kind switch
|
||||||
|
{
|
||||||
|
HistoryKind.Mp4 => "Screencast (MP4)",
|
||||||
|
HistoryKind.Gif => "Screencast (GIF)",
|
||||||
|
_ => "Knipsel"
|
||||||
|
};
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool FileExists => FilePath is { Length: > 0 } && File.Exists(FilePath);
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using Kniip.Infrastructure;
|
||||||
|
using Kniip.Settings;
|
||||||
|
|
||||||
|
namespace Kniip.History;
|
||||||
|
|
||||||
|
/// <summary>Wat er in kniip is opgeslagen of geüpload, nieuwste eerst.</summary>
|
||||||
|
public static class HistoryStore
|
||||||
|
{
|
||||||
|
private const int MaxEntries = 500;
|
||||||
|
private const int ThumbnailSize = 240;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions Options = new()
|
||||||
|
{
|
||||||
|
Converters = { new JsonStringEnumConverter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
private static List<HistoryEntry>? _entries;
|
||||||
|
|
||||||
|
private static string FilePath => Path.Combine(SettingsStore.Folder, "history.json");
|
||||||
|
|
||||||
|
public static string ThumbnailFolder => Path.Combine(SettingsStore.Folder, "history");
|
||||||
|
|
||||||
|
public static event Action? Changed;
|
||||||
|
|
||||||
|
public static IReadOnlyList<HistoryEntry> Entries => Load();
|
||||||
|
|
||||||
|
public static void Add(HistoryKind kind, string? filePath, string? url, BitmapSource? preview)
|
||||||
|
{
|
||||||
|
var entry = new HistoryEntry { Kind = kind, FilePath = filePath, Url = url };
|
||||||
|
if (preview != null)
|
||||||
|
{
|
||||||
|
entry.Width = preview.PixelWidth;
|
||||||
|
entry.Height = preview.PixelHeight;
|
||||||
|
entry.Thumbnail = SaveThumbnail(entry.Id, preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HistoryEntry> entries = Load();
|
||||||
|
entries.Insert(0, entry);
|
||||||
|
while (entries.Count > MaxEntries)
|
||||||
|
{
|
||||||
|
DeleteThumbnail(entries[^1]);
|
||||||
|
entries.RemoveAt(entries.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Remove(HistoryEntry entry)
|
||||||
|
{
|
||||||
|
if (Load().Remove(entry))
|
||||||
|
{
|
||||||
|
DeleteThumbnail(entry);
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Clear()
|
||||||
|
{
|
||||||
|
foreach (HistoryEntry entry in Load())
|
||||||
|
{
|
||||||
|
DeleteThumbnail(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Load().Clear();
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ThumbnailPath(HistoryEntry entry) =>
|
||||||
|
entry.Thumbnail is { Length: > 0 } name ? Path.Combine(ThumbnailFolder, name) : null;
|
||||||
|
|
||||||
|
/// <summary>Eerste beeld van een afbeelding of GIF, of null als dat niet lukt.</summary>
|
||||||
|
public static BitmapSource? PreviewOf(string file)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return PixelUtil.LoadFile(file);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<HistoryEntry> Load()
|
||||||
|
{
|
||||||
|
if (_entries != null)
|
||||||
|
{
|
||||||
|
return _entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(FilePath))
|
||||||
|
{
|
||||||
|
_entries = JsonSerializer.Deserialize<List<HistoryEntry>>(File.ReadAllText(FilePath), Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Beschadigd bestand: met een lege geschiedenis verder.
|
||||||
|
}
|
||||||
|
|
||||||
|
_entries ??= new List<HistoryEntry>();
|
||||||
|
_entries.RemoveAll(entry => entry == null!);
|
||||||
|
return _entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(SettingsStore.Folder);
|
||||||
|
File.WriteAllText(FilePath, JsonSerializer.Serialize(Load(), Options));
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// De geschiedenis bijwerken mag opslaan of uploaden nooit laten mislukken.
|
||||||
|
}
|
||||||
|
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? SaveThumbnail(string id, BitmapSource image)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
double scale = Math.Min(1.0, (double)ThumbnailSize / Math.Max(image.PixelWidth, image.PixelHeight));
|
||||||
|
BitmapSource thumb = scale < 1 ? new TransformedBitmap(image, new ScaleTransform(scale, scale)) : image;
|
||||||
|
var encoder = new PngBitmapEncoder();
|
||||||
|
encoder.Frames.Add(BitmapFrame.Create(thumb));
|
||||||
|
Directory.CreateDirectory(ThumbnailFolder);
|
||||||
|
string name = id + ".png";
|
||||||
|
using var stream = new FileStream(Path.Combine(ThumbnailFolder, name), FileMode.Create, FileAccess.Write);
|
||||||
|
encoder.Save(stream);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteThumbnail(HistoryEntry entry)
|
||||||
|
{
|
||||||
|
if (ThumbnailPath(entry) is not { } path)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Blijft staan; een losse miniatuur kan geen kwaad.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<HistoryEntry> Recent(int count) => Load().Take(count);
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<Window x:Class="Kniip.History.HistoryWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="kniip geschiedenis" Width="980" Height="620" MinWidth="560" MinHeight="360"
|
||||||
|
WindowStartupLocation="CenterScreen" Background="{DynamicResource WindowBrush}" Foreground="{DynamicResource InkBrush}"
|
||||||
|
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="ViewToggle" TargetType="RadioButton" BasedOn="{StaticResource ToolToggle}">
|
||||||
|
<Setter Property="Padding" Value="10,4" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="RowStyle" TargetType="ListViewItem">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
<EventSetter Event="MouseDoubleClick" Handler="Item_DoubleClick" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsAvailable}" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="TileStyle" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="Margin" Value="4" />
|
||||||
|
<EventSetter Event="MouseDoubleClick" Handler="Item_DoubleClick" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border x:Name="Bd" Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource ControlBorderBrush}" BorderThickness="1" CornerRadius="6" Padding="6">
|
||||||
|
<ContentPresenter />
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SelectedBrush}" />
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}" />
|
||||||
|
</Trigger>
|
||||||
|
<DataTrigger Binding="{Binding IsAvailable}" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</DataTrigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<DataTemplate x:Key="SmallThumb">
|
||||||
|
<Grid Width="40" Height="28">
|
||||||
|
<Image Source="{Binding Thumbnail}" Stretch="Uniform" RenderOptions.BitmapScalingMode="HighQuality" />
|
||||||
|
<TextBlock Text="" Style="{StaticResource IconText}" Foreground="{DynamicResource SubtleTextBrush}"
|
||||||
|
Visibility="{Binding NoThumbnailVisibility}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<DockPanel Margin="12">
|
||||||
|
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,10" LastChildFill="False">
|
||||||
|
<RadioButton x:Name="ListToggle" Style="{StaticResource ViewToggle}" GroupName="View" Checked="View_Checked">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" Style="{StaticResource IconText}" Margin="0,0,6,0" />
|
||||||
|
<TextBlock Text="Lijst" VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</RadioButton>
|
||||||
|
<RadioButton x:Name="GridToggle" Style="{StaticResource ViewToggle}" GroupName="View" Checked="View_Checked">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" Style="{StaticResource IconText}" Margin="0,0,6,0" />
|
||||||
|
<TextBlock Text="Miniaturen" VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</RadioButton>
|
||||||
|
<Grid DockPanel.Dock="Right" Width="260" VerticalAlignment="Center">
|
||||||
|
<TextBox x:Name="SearchBox" Height="30" Padding="26,0,6,0" VerticalContentAlignment="Center"
|
||||||
|
TextChanged="SearchBox_TextChanged" />
|
||||||
|
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="13" HorizontalAlignment="Left" Margin="8,0,0,0"
|
||||||
|
Foreground="{DynamicResource SubtleTextBrush}" IsHitTestVisible="False" />
|
||||||
|
<TextBlock x:Name="SearchHint" Text="Zoeken op naam of link" Margin="27,0,0,0" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource SubtleTextBrush}" IsHitTestVisible="False" />
|
||||||
|
</Grid>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<DockPanel DockPanel.Dock="Bottom" Margin="0,10,0,0" LastChildFill="False">
|
||||||
|
<TextBlock x:Name="CountText" VerticalAlignment="Center" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
|
<Button DockPanel.Dock="Right" Content="Sluiten" Style="{StaticResource SecondaryButton}" IsCancel="True" Click="Close_Click" />
|
||||||
|
<Button x:Name="ClearButton" DockPanel.Dock="Right" Content="Geschiedenis wissen..." Style="{StaticResource SecondaryButton}"
|
||||||
|
Click="Clear_Click" />
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<ListView x:Name="ListItems" ItemContainerStyle="{StaticResource RowStyle}" SelectionMode="Single"
|
||||||
|
Background="{DynamicResource CardBrush}" Foreground="{DynamicResource InkBrush}"
|
||||||
|
BorderBrush="{DynamicResource ControlBorderBrush}" ContextMenuOpening="Items_ContextMenuOpening">
|
||||||
|
<ListView.ContextMenu>
|
||||||
|
<ContextMenu />
|
||||||
|
</ListView.ContextMenu>
|
||||||
|
<ListView.View>
|
||||||
|
<GridView>
|
||||||
|
<GridViewColumn Width="56" CellTemplate="{StaticResource SmallThumb}" />
|
||||||
|
<GridViewColumn Header="Datum" Width="140" DisplayMemberBinding="{Binding TimeText}" />
|
||||||
|
<GridViewColumn Header="Soort" Width="120" DisplayMemberBinding="{Binding Entry.KindText}" />
|
||||||
|
<GridViewColumn Header="Naam" Width="200" DisplayMemberBinding="{Binding Entry.Name}" />
|
||||||
|
<GridViewColumn Header="Map" Width="200" DisplayMemberBinding="{Binding Folder}" />
|
||||||
|
<GridViewColumn Header="Link" Width="220" DisplayMemberBinding="{Binding Entry.Url}" />
|
||||||
|
</GridView>
|
||||||
|
</ListView.View>
|
||||||
|
</ListView>
|
||||||
|
|
||||||
|
<ListBox x:Name="GridItems" ItemContainerStyle="{StaticResource TileStyle}" SelectionMode="Single" Visibility="Collapsed"
|
||||||
|
Background="Transparent" BorderThickness="0" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
|
ContextMenuOpening="Items_ContextMenuOpening">
|
||||||
|
<ListBox.ContextMenu>
|
||||||
|
<ContextMenu />
|
||||||
|
</ListBox.ContextMenu>
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Width="180" ToolTip="{Binding ToolTip}">
|
||||||
|
<Grid Height="120" Background="{DynamicResource WindowBrush}">
|
||||||
|
<Image Source="{Binding Thumbnail}" Stretch="Uniform" RenderOptions.BitmapScalingMode="HighQuality" />
|
||||||
|
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="32"
|
||||||
|
Foreground="{DynamicResource SubtleTextBrush}" Visibility="{Binding NoThumbnailVisibility}" />
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding Entry.Name}" Margin="0,6,0,0" TextTrimming="CharacterEllipsis" />
|
||||||
|
<TextBlock Text="{Binding TimeText}" FontSize="12" Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<TextBlock x:Name="EmptyText" HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"
|
||||||
|
Foreground="{DynamicResource SubtleTextBrush}" Visibility="Collapsed"
|
||||||
|
Text="Nog niets in de geschiedenis.
Wat je in kniip opslaat of uploadt, komt hier te staan." />
|
||||||
|
</Grid>
|
||||||
|
</DockPanel>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
|
namespace Kniip.History;
|
||||||
|
|
||||||
|
public partial class HistoryWindow : Window
|
||||||
|
{
|
||||||
|
private List<HistoryRow> _rows = new();
|
||||||
|
|
||||||
|
public HistoryWindow(bool images)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Infrastructure.ThemeManager.Attach(this);
|
||||||
|
Reload();
|
||||||
|
ShowImages(images);
|
||||||
|
HistoryStore.Changed += Reload;
|
||||||
|
Closed += (_, _) => HistoryStore.Changed -= Reload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowImages(bool images)
|
||||||
|
{
|
||||||
|
(images ? GridToggle : ListToggle).IsChecked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reload()
|
||||||
|
{
|
||||||
|
_rows = HistoryStore.Entries.Select(entry => new HistoryRow(entry)).ToList();
|
||||||
|
ListItems.ItemsSource = _rows;
|
||||||
|
GridItems.ItemsSource = _rows;
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFilter()
|
||||||
|
{
|
||||||
|
string query = SearchBox.Text.Trim();
|
||||||
|
ICollectionView view = System.Windows.Data.CollectionViewSource.GetDefaultView(_rows);
|
||||||
|
view.Filter = query.Length == 0 ? null : item => ((HistoryRow)item).Matches(query);
|
||||||
|
int shown = view.Cast<object>().Count();
|
||||||
|
CountText.Text = query.Length == 0 ? $"{_rows.Count} items" : $"{shown} van {_rows.Count} items";
|
||||||
|
EmptyText.Visibility = _rows.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
ClearButton.IsEnabled = _rows.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void View_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
bool images = GridToggle.IsChecked == true;
|
||||||
|
GridItems.Visibility = images ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
ListItems.Visibility = images ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
Title = images ? "kniip afbeeldingsgeschiedenis" : "kniip geschiedenis";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
SearchHint.Visibility = SearchBox.Text.Length == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Item_DoubleClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is FrameworkElement { DataContext: HistoryRow row })
|
||||||
|
{
|
||||||
|
HistoryActions.Open(row.Entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Items_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||||
|
{
|
||||||
|
var list = (Selector)sender;
|
||||||
|
if (e.OriginalSource is DependencyObject source
|
||||||
|
&& ItemsControl.ContainerFromElement(list, source) is FrameworkElement { DataContext: HistoryRow clicked })
|
||||||
|
{
|
||||||
|
list.SelectedItem = clicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.SelectedItem is not HistoryRow row || list.ContextMenu is not { } menu)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.Items.Clear();
|
||||||
|
foreach (HistoryCommand? command in HistoryActions.CommandsFor(row.Entry))
|
||||||
|
{
|
||||||
|
menu.Items.Add(command == null ? new Separator() : MenuItemFor(command.Label, command.Glyph, command.Enabled, command.Run));
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.Items.Add(new Separator());
|
||||||
|
menu.Items.Add(MenuItemFor("Uit geschiedenis verwijderen", "\uE74D", true, () => HistoryStore.Remove(row.Entry)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MenuItem MenuItemFor(string label, string glyph, bool enabled, Action run)
|
||||||
|
{
|
||||||
|
var item = new MenuItem
|
||||||
|
{
|
||||||
|
Header = label,
|
||||||
|
IsEnabled = enabled,
|
||||||
|
Icon = new TextBlock { Text = glyph, Style = (Style)Application.Current.FindResource("IconText"), FontSize = 14 }
|
||||||
|
};
|
||||||
|
item.Click += (_, _) => run();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Clear_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (MessageBox.Show(this, "De hele geschiedenis wissen? Je opgeslagen bestanden en uploads blijven gewoon bestaan.", "kniip",
|
||||||
|
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
HistoryStore.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||||
|
|
||||||
|
private sealed class HistoryRow
|
||||||
|
{
|
||||||
|
public HistoryRow(HistoryEntry entry)
|
||||||
|
{
|
||||||
|
Entry = entry;
|
||||||
|
IsAvailable = entry.FileExists || entry.Url is { Length: > 0 };
|
||||||
|
Thumbnail = LoadThumbnail(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HistoryEntry Entry { get; }
|
||||||
|
public bool IsAvailable { get; }
|
||||||
|
public ImageSource? Thumbnail { get; }
|
||||||
|
public Visibility NoThumbnailVisibility => Thumbnail == null ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
public string TimeText => Entry.Time.ToString("dd-MM-yyyy HH:mm:ss");
|
||||||
|
public string? Folder => Entry.FilePath is { Length: > 0 } path ? Path.GetDirectoryName(path) : null;
|
||||||
|
|
||||||
|
public string ToolTip => string.Join("\n", new[]
|
||||||
|
{
|
||||||
|
Entry.KindText + (Entry.Width > 0 ? $", {Entry.Width} x {Entry.Height}" : ""),
|
||||||
|
Entry.FilePath,
|
||||||
|
Entry.Url
|
||||||
|
}.Where(line => !string.IsNullOrEmpty(line)));
|
||||||
|
|
||||||
|
public bool Matches(string query) =>
|
||||||
|
Entry.Name.Contains(query, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (Entry.FilePath?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||||
|
|| (Entry.Url?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||||
|
|
||||||
|
private static ImageSource? LoadThumbnail(HistoryEntry entry)
|
||||||
|
{
|
||||||
|
if (HistoryStore.ThumbnailPath(entry) is not { } path || !File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var image = new BitmapImage();
|
||||||
|
image.BeginInit();
|
||||||
|
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
|
image.StreamSource = new MemoryStream(File.ReadAllBytes(path));
|
||||||
|
image.EndInit();
|
||||||
|
image.Freeze();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Kniip.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Namen waaronder kniip zich in Windows inschrijft. De testbuild (<c>-p:KniipTest=true</c>) gebruikt eigen
|
||||||
|
/// namen voor register, instellingen, mutex en tijdelijke bestanden, zodat hij naast de geïnstalleerde
|
||||||
|
/// kniip kan draaien en die nooit overschrijft.
|
||||||
|
/// </summary>
|
||||||
|
internal static class AppIdentity
|
||||||
|
{
|
||||||
|
#if KNIIP_TEST
|
||||||
|
public static bool IsTest => true;
|
||||||
|
public const string Key = "KniipTest";
|
||||||
|
public const string DisplayName = "kniip test";
|
||||||
|
#else
|
||||||
|
public static bool IsTest => false;
|
||||||
|
public const string Key = "Kniip";
|
||||||
|
public const string DisplayName = "kniip";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -9,10 +9,10 @@ namespace Kniip.Infrastructure;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class ShellIntegration
|
internal static class ShellIntegration
|
||||||
{
|
{
|
||||||
private const string ProgId = "Kniip.Image";
|
private const string ProgId = AppIdentity.Key + ".Image";
|
||||||
private const string AppKeyName = "Kniip.exe";
|
private const string AppKeyName = AppIdentity.Key + ".exe";
|
||||||
private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||||
private const string RunValueName = "Kniip";
|
private const string RunValueName = AppIdentity.Key;
|
||||||
|
|
||||||
public static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".bmp", ".gif" };
|
public static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".bmp", ".gif" };
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ internal static class ShellIntegration
|
|||||||
{
|
{
|
||||||
using (var prog = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}"))
|
using (var prog = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}"))
|
||||||
{
|
{
|
||||||
prog.SetValue(null, "kniip afbeelding");
|
prog.SetValue(null, $"{AppIdentity.DisplayName} afbeelding");
|
||||||
prog.SetValue("FriendlyTypeName", "kniip afbeelding");
|
prog.SetValue("FriendlyTypeName", $"{AppIdentity.DisplayName} afbeelding");
|
||||||
}
|
}
|
||||||
using (var icon = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}\DefaultIcon"))
|
using (var icon = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}\DefaultIcon"))
|
||||||
{
|
{
|
||||||
@@ -46,7 +46,7 @@ internal static class ShellIntegration
|
|||||||
|
|
||||||
using (var app = Registry.CurrentUser.CreateSubKey($@"Software\Classes\Applications\{AppKeyName}"))
|
using (var app = Registry.CurrentUser.CreateSubKey($@"Software\Classes\Applications\{AppKeyName}"))
|
||||||
{
|
{
|
||||||
app.SetValue("FriendlyAppName", "kniip editor");
|
app.SetValue("FriendlyAppName", $"{AppIdentity.DisplayName} editor");
|
||||||
}
|
}
|
||||||
using (var appCmd = Registry.CurrentUser.CreateSubKey($@"Software\Classes\Applications\{AppKeyName}\shell\open\command"))
|
using (var appCmd = Registry.CurrentUser.CreateSubKey($@"Software\Classes\Applications\{AppKeyName}\shell\open\command"))
|
||||||
{
|
{
|
||||||
@@ -89,6 +89,11 @@ internal static class ShellIntegration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static void MigrateLegacyRegistration()
|
public static void MigrateLegacyRegistration()
|
||||||
{
|
{
|
||||||
|
if (AppIdentity.IsTest)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const string legacyProgId = "KnipPlus.Image";
|
const string legacyProgId = "KnipPlus.Image";
|
||||||
const string legacyAppKeyName = "KnipPlus.exe";
|
const string legacyAppKeyName = "KnipPlus.exe";
|
||||||
const string legacyRunValueName = "KnipPlus";
|
const string legacyRunValueName = "KnipPlus";
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Kniip.Infrastructure;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class SingleInstance : IDisposable
|
internal sealed class SingleInstance : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly string BaseName = "Kniip-" + Environment.UserName;
|
private static readonly string BaseName = AppIdentity.Key + "-" + Environment.UserName;
|
||||||
private static readonly string PipeName = BaseName + "-pipe";
|
private static readonly string PipeName = BaseName + "-pipe";
|
||||||
|
|
||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using Kniip.Interop;
|
||||||
|
using Kniip.Settings;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace Kniip.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>Wisselt de kleuren van alle vensters tussen licht en donker (Themes/Light.xaml en Themes/Dark.xaml).</summary>
|
||||||
|
internal static class ThemeManager
|
||||||
|
{
|
||||||
|
private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
|
||||||
|
|
||||||
|
private static readonly Uri LightUri = new("pack://application:,,,/Themes/Light.xaml");
|
||||||
|
private static readonly Uri DarkUri = new("pack://application:,,,/Themes/Dark.xaml");
|
||||||
|
|
||||||
|
private static ResourceDictionary? _light;
|
||||||
|
private static ResourceDictionary? _dark;
|
||||||
|
private static ColorMode _mode;
|
||||||
|
private static bool _listening;
|
||||||
|
|
||||||
|
public static bool IsDark { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Na elke wissel tussen licht en donker.</summary>
|
||||||
|
public static event Action? Changed;
|
||||||
|
|
||||||
|
public static void Apply(ColorMode mode)
|
||||||
|
{
|
||||||
|
_mode = mode;
|
||||||
|
if (mode == ColorMode.System && !_listening)
|
||||||
|
{
|
||||||
|
_listening = true;
|
||||||
|
SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Geeft het venster een titelbalk in de huidige kleurmodus, ook na een latere wissel.</summary>
|
||||||
|
public static void Attach(Window window) =>
|
||||||
|
window.SourceInitialized += (_, _) => NativeMethods.SetDarkTitleBar(new WindowInteropHelper(window).Handle, IsDark);
|
||||||
|
|
||||||
|
private static void Refresh()
|
||||||
|
{
|
||||||
|
bool dark = _mode switch
|
||||||
|
{
|
||||||
|
ColorMode.Dark => true,
|
||||||
|
ColorMode.System => IsSystemDark(),
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
if (dark == IsDark && _light != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsDark = dark;
|
||||||
|
_light ??= new ResourceDictionary { Source = LightUri };
|
||||||
|
_dark ??= new ResourceDictionary { Source = DarkUri };
|
||||||
|
|
||||||
|
var merged = Application.Current.Resources.MergedDictionaries;
|
||||||
|
int index = -1;
|
||||||
|
for (int i = 0; i < merged.Count; i++)
|
||||||
|
{
|
||||||
|
if (merged[i].Source is { } source && (source.OriginalString.EndsWith("Light.xaml", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| source.OriginalString.EndsWith("Dark.xaml", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceDictionary theme = dark ? _dark : _light;
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
merged.Insert(0, theme);
|
||||||
|
}
|
||||||
|
else if (merged[index] != theme)
|
||||||
|
{
|
||||||
|
merged[index] = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Window window in Application.Current.Windows)
|
||||||
|
{
|
||||||
|
IntPtr hwnd = new WindowInteropHelper(window).Handle;
|
||||||
|
if (hwnd != IntPtr.Zero && window.WindowStyle != WindowStyle.None)
|
||||||
|
{
|
||||||
|
NativeMethods.SetDarkTitleBar(hwnd, dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Category == UserPreferenceCategory.General)
|
||||||
|
{
|
||||||
|
Application.Current?.Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
if (_mode == ColorMode.System)
|
||||||
|
{
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSystemDark()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using RegistryKey? key = Registry.CurrentUser.OpenSubKey(PersonalizeKey);
|
||||||
|
return key?.GetValue("AppsUseLightTheme") is int light && light == 0;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using Kniip.History;
|
||||||
|
|
||||||
namespace Kniip.Infrastructure;
|
namespace Kniip.Infrastructure;
|
||||||
|
|
||||||
@@ -9,11 +13,24 @@ internal sealed class TrayIcon : IDisposable
|
|||||||
private readonly NotifyIcon _icon;
|
private readonly NotifyIcon _icon;
|
||||||
private const string RecordText = "Screencast opnemen (MP4)...";
|
private const string RecordText = "Screencast opnemen (MP4)...";
|
||||||
|
|
||||||
|
private readonly ToolStripMenuItem _clipboardItem;
|
||||||
private readonly ToolStripMenuItem _pauseItem;
|
private readonly ToolStripMenuItem _pauseItem;
|
||||||
private readonly ToolStripMenuItem _snipItem;
|
private readonly ToolStripMenuItem _snipItem;
|
||||||
private readonly ToolStripMenuItem _windowItem;
|
private readonly ToolStripMenuItem _windowItem;
|
||||||
private readonly ToolStripMenuItem _recordItem;
|
private readonly ToolStripMenuItem _recordItem;
|
||||||
private readonly ToolStripMenuItem _gifItem;
|
private readonly ToolStripMenuItem _gifItem;
|
||||||
|
private readonly ToolStripMenuItem _foldersItem;
|
||||||
|
private readonly ToolStripMenuItem _recentItem;
|
||||||
|
private readonly ToolStripMenuItem _delayItem;
|
||||||
|
private Panel _delayPanel = null!;
|
||||||
|
private TrackBar _delaySlider = null!;
|
||||||
|
private NumericUpDown _delayNumber = null!;
|
||||||
|
private Label _delayUnit = null!;
|
||||||
|
private int _delaySeconds;
|
||||||
|
private int _savedDelaySeconds;
|
||||||
|
|
||||||
|
/// <summary>Nieuwe vertraging in seconden, afgevuurd bij het sluiten van het submenu als de waarde is gewijzigd.</summary>
|
||||||
|
public event Action<int>? CaptureDelayChanged;
|
||||||
|
|
||||||
public event Action? OpenFromClipboardRequested;
|
public event Action? OpenFromClipboardRequested;
|
||||||
public event Action? SnipRequested;
|
public event Action? SnipRequested;
|
||||||
@@ -25,26 +42,46 @@ internal sealed class TrayIcon : IDisposable
|
|||||||
public event Action? ExitRequested;
|
public event Action? ExitRequested;
|
||||||
public event Action<bool>? PauseChanged;
|
public event Action<bool>? PauseChanged;
|
||||||
|
|
||||||
|
/// <summary>Geschiedenisvenster openen; true = met miniaturen.</summary>
|
||||||
|
public event Action<bool>? HistoryRequested;
|
||||||
|
|
||||||
|
/// <summary>Mappen voor het submenu Map openen, opgevraagd bij het openen; een pad van null staat grijs.</summary>
|
||||||
|
public Func<IReadOnlyList<(string Label, string? Path)>>? FoldersProvider { get; set; }
|
||||||
|
|
||||||
public TrayIcon()
|
public TrayIcon()
|
||||||
{
|
{
|
||||||
var menu = new ContextMenuStrip();
|
var menu = new ContextMenuStrip();
|
||||||
var openClipboard = new ToolStripMenuItem("Bewerken vanaf klembord", null, (_, _) => OpenFromClipboardRequested?.Invoke())
|
_clipboardItem = new ToolStripMenuItem("Bewerken vanaf klembord", null, (_, _) => OpenFromClipboardRequested?.Invoke())
|
||||||
{
|
{
|
||||||
Font = new Font(SystemFonts.MenuFont ?? Control.DefaultFont, FontStyle.Bold)
|
Font = new Font(SystemFonts.MenuFont ?? Control.DefaultFont, FontStyle.Bold)
|
||||||
};
|
};
|
||||||
menu.Items.Add(openClipboard);
|
menu.Items.Add(_clipboardItem);
|
||||||
menu.Items.Add("Afbeelding openen...", null, (_, _) => OpenFileRequested?.Invoke());
|
menu.Items.Add("Afbeelding openen...", null, (_, _) => OpenFileRequested?.Invoke());
|
||||||
menu.Items.Add(new ToolStripSeparator());
|
menu.Items.Add(new ToolStripSeparator());
|
||||||
_snipItem = new ToolStripMenuItem("Rechthoek knippen", null, (_, _) => SnipRequested?.Invoke());
|
_snipItem = new ToolStripMenuItem("Rechthoek knippen", null, (_, _) => SnipRequested?.Invoke());
|
||||||
menu.Items.Add(_snipItem);
|
menu.Items.Add(_snipItem);
|
||||||
_windowItem = new ToolStripMenuItem("Venster knippen", null, (_, _) => WindowSnipRequested?.Invoke());
|
_windowItem = new ToolStripMenuItem("Venster knippen", null, (_, _) => WindowSnipRequested?.Invoke());
|
||||||
menu.Items.Add(_windowItem);
|
menu.Items.Add(_windowItem);
|
||||||
|
_delayItem = BuildDelayItem();
|
||||||
|
menu.Items.Add(_delayItem);
|
||||||
_recordItem = new ToolStripMenuItem(RecordText, null, (_, _) => RecordRequested?.Invoke());
|
_recordItem = new ToolStripMenuItem(RecordText, null, (_, _) => RecordRequested?.Invoke());
|
||||||
menu.Items.Add(_recordItem);
|
menu.Items.Add(_recordItem);
|
||||||
_gifItem = new ToolStripMenuItem("Screencast opnemen (GIF)...", null, (_, _) => GifRequested?.Invoke());
|
_gifItem = new ToolStripMenuItem("Screencast opnemen (GIF)...", null, (_, _) => GifRequested?.Invoke());
|
||||||
menu.Items.Add(_gifItem);
|
menu.Items.Add(_gifItem);
|
||||||
menu.Items.Add(new ToolStripSeparator());
|
menu.Items.Add(new ToolStripSeparator());
|
||||||
|
|
||||||
|
_foldersItem = new ToolStripMenuItem("Map openen");
|
||||||
|
_foldersItem.DropDownItems.Add(new ToolStripMenuItem("..."));
|
||||||
|
_foldersItem.DropDownOpening += (_, _) => BuildFolders();
|
||||||
|
menu.Items.Add(_foldersItem);
|
||||||
|
menu.Items.Add("Geschiedenis...", null, (_, _) => HistoryRequested?.Invoke(false));
|
||||||
|
menu.Items.Add("Afbeeldingsgeschiedenis...", null, (_, _) => HistoryRequested?.Invoke(true));
|
||||||
|
_recentItem = new ToolStripMenuItem("Recente items");
|
||||||
|
_recentItem.DropDownItems.Add(new ToolStripMenuItem("..."));
|
||||||
|
_recentItem.DropDownOpening += (_, _) => BuildRecent();
|
||||||
|
menu.Items.Add(_recentItem);
|
||||||
|
menu.Items.Add(new ToolStripSeparator());
|
||||||
|
|
||||||
_pauseItem = new ToolStripMenuItem("Knipsels niet automatisch oppikken") { CheckOnClick = true };
|
_pauseItem = new ToolStripMenuItem("Knipsels niet automatisch oppikken") { CheckOnClick = true };
|
||||||
_pauseItem.CheckedChanged += (_, _) => PauseChanged?.Invoke(_pauseItem.Checked);
|
_pauseItem.CheckedChanged += (_, _) => PauseChanged?.Invoke(_pauseItem.Checked);
|
||||||
menu.Items.Add(_pauseItem);
|
menu.Items.Add(_pauseItem);
|
||||||
@@ -55,7 +92,7 @@ internal sealed class TrayIcon : IDisposable
|
|||||||
_icon = new NotifyIcon
|
_icon = new NotifyIcon
|
||||||
{
|
{
|
||||||
Icon = LoadIcon(),
|
Icon = LoadIcon(),
|
||||||
Text = "kniip - annotaties voor het Knipprogramma",
|
Text = IdleText,
|
||||||
ContextMenuStrip = menu,
|
ContextMenuStrip = menu,
|
||||||
Visible = true
|
Visible = true
|
||||||
};
|
};
|
||||||
@@ -71,8 +108,9 @@ internal sealed class TrayIcon : IDisposable
|
|||||||
public bool IsPaused => _pauseItem.Checked;
|
public bool IsPaused => _pauseItem.Checked;
|
||||||
|
|
||||||
/// <summary>Toont de sneltoetsen naast de menu-items; null = geen sneltoets.</summary>
|
/// <summary>Toont de sneltoetsen naast de menu-items; null = geen sneltoets.</summary>
|
||||||
public void SetHotkeys(string? snip, string? window, string? record, string? gif)
|
public void SetHotkeys(string? snip, string? window, string? record, string? gif, string? clipboard)
|
||||||
{
|
{
|
||||||
|
_clipboardItem.ShortcutKeyDisplayString = clipboard;
|
||||||
_snipItem.ShortcutKeyDisplayString = snip;
|
_snipItem.ShortcutKeyDisplayString = snip;
|
||||||
_windowItem.ShortcutKeyDisplayString = window;
|
_windowItem.ShortcutKeyDisplayString = window;
|
||||||
_recordItem.ShortcutKeyDisplayString = record;
|
_recordItem.ShortcutKeyDisplayString = record;
|
||||||
@@ -84,7 +122,193 @@ internal sealed class TrayIcon : IDisposable
|
|||||||
{
|
{
|
||||||
_recordItem.Text = text ?? RecordText;
|
_recordItem.Text = text ?? RecordText;
|
||||||
_gifItem.Visible = text == null;
|
_gifItem.Visible = text == null;
|
||||||
_icon.Text = recording ? "kniip - opname loopt" : "kniip - annotaties voor het Knipprogramma";
|
_icon.Text = recording ? $"{AppIdentity.DisplayName} - opname loopt" : IdleText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCaptureDelay(int seconds)
|
||||||
|
{
|
||||||
|
_savedDelaySeconds = seconds;
|
||||||
|
ShowCaptureDelay(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ToolStripMenuItem BuildDelayItem()
|
||||||
|
{
|
||||||
|
var item = new ToolStripMenuItem();
|
||||||
|
_delaySlider = new TrackBar
|
||||||
|
{
|
||||||
|
Minimum = 0,
|
||||||
|
Maximum = Kniip.Settings.AppSettings.MaxCaptureDelaySeconds,
|
||||||
|
TickFrequency = 5,
|
||||||
|
LargeChange = 5,
|
||||||
|
SmallChange = 1,
|
||||||
|
AutoSize = false,
|
||||||
|
Width = 200,
|
||||||
|
Height = 32,
|
||||||
|
Location = new Point(4, 6)
|
||||||
|
};
|
||||||
|
_delayNumber = new NumericUpDown
|
||||||
|
{
|
||||||
|
Minimum = 0,
|
||||||
|
Maximum = Kniip.Settings.AppSettings.MaxCaptureDelaySeconds,
|
||||||
|
Width = 52,
|
||||||
|
Location = new Point(_delaySlider.Right + 6, 10),
|
||||||
|
TextAlign = HorizontalAlignment.Right
|
||||||
|
};
|
||||||
|
_delayUnit = new Label
|
||||||
|
{
|
||||||
|
Text = "seconden",
|
||||||
|
AutoSize = true,
|
||||||
|
Location = new Point(_delayNumber.Right + 4, 13)
|
||||||
|
};
|
||||||
|
var panel = new Panel
|
||||||
|
{
|
||||||
|
Size = new Size(_delayUnit.Left + _delayUnit.PreferredWidth + 10, 44),
|
||||||
|
BackColor = ProfessionalColors.ToolStripDropDownBackground
|
||||||
|
};
|
||||||
|
_delayPanel = panel;
|
||||||
|
_delaySlider.BackColor = panel.BackColor;
|
||||||
|
panel.Controls.AddRange(new Control[] { _delaySlider, _delayNumber, _delayUnit });
|
||||||
|
|
||||||
|
_delaySlider.ValueChanged += (_, _) => ShowCaptureDelay(_delaySlider.Value);
|
||||||
|
_delayNumber.ValueChanged += (_, _) => ShowCaptureDelay((int)_delayNumber.Value);
|
||||||
|
_delayNumber.KeyDown += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.KeyCode is Keys.Enter or Keys.Escape)
|
||||||
|
{
|
||||||
|
e.SuppressKeyPress = true;
|
||||||
|
_icon.ContextMenuStrip?.Close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
item.DropDownItems.Add(new ToolStripControlHost(panel) { AutoSize = false, Size = panel.Size, Margin = Padding.Empty });
|
||||||
|
item.DropDownClosed += (_, _) =>
|
||||||
|
{
|
||||||
|
ShowCaptureDelay((int)_delayNumber.Value);
|
||||||
|
if (_delaySeconds != _savedDelaySeconds)
|
||||||
|
{
|
||||||
|
_savedDelaySeconds = _delaySeconds;
|
||||||
|
CaptureDelayChanged?.Invoke(_delaySeconds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
item.Text = "Schermafdruk vertraging: 0s";
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowCaptureDelay(int seconds)
|
||||||
|
{
|
||||||
|
_delaySeconds = seconds;
|
||||||
|
if (_delaySlider.Value != seconds)
|
||||||
|
{
|
||||||
|
_delaySlider.Value = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)_delayNumber.Value != seconds)
|
||||||
|
{
|
||||||
|
_delayNumber.Value = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
_delayItem.Text = $"Schermafdruk vertraging: {seconds}s";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Menu in de kleurmodus van kniip.</summary>
|
||||||
|
public void ApplyTheme(bool dark)
|
||||||
|
{
|
||||||
|
ToolStripManager.Renderer = dark ? new DarkMenuRenderer() : new ToolStripProfessionalRenderer();
|
||||||
|
Color back = dark ? DarkMenuColors.Background : ProfessionalColors.ToolStripDropDownBackground;
|
||||||
|
Color fore = dark ? DarkMenuColors.Text : SystemColors.ControlText;
|
||||||
|
_delayPanel.BackColor = back;
|
||||||
|
_delaySlider.BackColor = back;
|
||||||
|
_delayUnit.ForeColor = fore;
|
||||||
|
_delayNumber.BackColor = dark ? DarkMenuColors.Input : SystemColors.Window;
|
||||||
|
_delayNumber.ForeColor = dark ? DarkMenuColors.Text : SystemColors.WindowText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string IdleText => AppIdentity.IsTest
|
||||||
|
? $"{AppIdentity.DisplayName} {Kniip.Update.UpdateChecker.CurrentVersion}"
|
||||||
|
: AppIdentity.DisplayName;
|
||||||
|
|
||||||
|
private void BuildFolders()
|
||||||
|
{
|
||||||
|
_foldersItem.DropDownItems.Clear();
|
||||||
|
foreach ((string label, string? path) in FoldersProvider?.Invoke() ?? Array.Empty<(string, string?)>())
|
||||||
|
{
|
||||||
|
var item = new ToolStripMenuItem(label) { Enabled = path != null, ToolTipText = path };
|
||||||
|
if (path != null)
|
||||||
|
{
|
||||||
|
item.Click += (_, _) => HistoryActions.OpenFolder(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
_foldersItem.DropDownItems.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildRecent()
|
||||||
|
{
|
||||||
|
foreach (ToolStripItem old in _recentItem.DropDownItems)
|
||||||
|
{
|
||||||
|
old.Image?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_recentItem.DropDownItems.Clear();
|
||||||
|
List<HistoryEntry> entries = HistoryStore.Recent(10).ToList();
|
||||||
|
if (entries.Count == 0)
|
||||||
|
{
|
||||||
|
_recentItem.DropDownItems.Add(new ToolStripMenuItem("Nog niets opgeslagen") { Enabled = false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (HistoryEntry entry in entries)
|
||||||
|
{
|
||||||
|
var item = new ToolStripMenuItem($"[{entry.Time:HH:mm:ss}] {entry.Url ?? entry.Name}")
|
||||||
|
{
|
||||||
|
Image = LoadThumbnail(entry),
|
||||||
|
ToolTipText = entry.FilePath
|
||||||
|
};
|
||||||
|
item.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
_icon.ContextMenuStrip?.Close();
|
||||||
|
HistoryActions.Open(entry);
|
||||||
|
};
|
||||||
|
foreach (HistoryCommand? command in HistoryActions.CommandsFor(entry))
|
||||||
|
{
|
||||||
|
if (command == null)
|
||||||
|
{
|
||||||
|
item.DropDownItems.Add(new ToolStripSeparator());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item.DropDownItems.Add(new ToolStripMenuItem(command.Label, null, (_, _) => command.Run()) { Enabled = command.Enabled });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_recentItem.DropDownItems.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Image? LoadThumbnail(HistoryEntry entry)
|
||||||
|
{
|
||||||
|
if (HistoryStore.ThumbnailPath(entry) is not { } path || !File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(File.ReadAllBytes(path));
|
||||||
|
using var image = Image.FromStream(stream);
|
||||||
|
Size size = SystemInformation.SmallIconSize;
|
||||||
|
double scale = Math.Min((double)size.Width / image.Width, (double)size.Height / image.Height);
|
||||||
|
var thumb = new Bitmap(size.Width, size.Height);
|
||||||
|
using Graphics g = Graphics.FromImage(thumb);
|
||||||
|
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||||
|
int w = Math.Max(1, (int)(image.Width * scale)), h = Math.Max(1, (int)(image.Height * scale));
|
||||||
|
g.DrawImage(image, (size.Width - w) / 2, (size.Height - h) / 2, w, h);
|
||||||
|
return thumb;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowBalloon(string title, string text) =>
|
public void ShowBalloon(string title, string text) =>
|
||||||
@@ -92,7 +316,8 @@ internal sealed class TrayIcon : IDisposable
|
|||||||
|
|
||||||
private static Icon LoadIcon()
|
private static Icon LoadIcon()
|
||||||
{
|
{
|
||||||
var info = System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/AppIcon.ico"));
|
// Uitsnede van AppIcon.ico zonder snijlijnen (tools\make-tray-icon.ps1): op 16px vult het kader zo het vak.
|
||||||
|
var info = System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/TrayIcon.ico"));
|
||||||
if (info == null)
|
if (info == null)
|
||||||
{
|
{
|
||||||
return SystemIcons.Application;
|
return SystemIcons.Application;
|
||||||
@@ -107,4 +332,68 @@ internal sealed class TrayIcon : IDisposable
|
|||||||
_icon.Visible = false;
|
_icon.Visible = false;
|
||||||
_icon.Dispose();
|
_icon.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class DarkMenuColors : ProfessionalColorTable
|
||||||
|
{
|
||||||
|
public static readonly Color Background = Color.FromArgb(0x2B, 0x2B, 0x2B);
|
||||||
|
public static readonly Color Highlight = Color.FromArgb(0x3D, 0x3D, 0x3D);
|
||||||
|
public static readonly Color Border = Color.FromArgb(0x45, 0x45, 0x45);
|
||||||
|
public static readonly Color Input = Color.FromArgb(0x2D, 0x2D, 0x2D);
|
||||||
|
public static readonly Color Text = Color.FromArgb(0xF2, 0xF2, 0xF2);
|
||||||
|
|
||||||
|
public override Color ToolStripDropDownBackground => Background;
|
||||||
|
public override Color ImageMarginGradientBegin => Background;
|
||||||
|
public override Color ImageMarginGradientMiddle => Background;
|
||||||
|
public override Color ImageMarginGradientEnd => Background;
|
||||||
|
public override Color MenuBorder => Border;
|
||||||
|
public override Color MenuItemBorder => Highlight;
|
||||||
|
public override Color MenuItemSelected => Highlight;
|
||||||
|
public override Color MenuItemSelectedGradientBegin => Highlight;
|
||||||
|
public override Color MenuItemSelectedGradientEnd => Highlight;
|
||||||
|
public override Color MenuItemPressedGradientBegin => Highlight;
|
||||||
|
public override Color MenuItemPressedGradientEnd => Highlight;
|
||||||
|
public override Color SeparatorDark => Border;
|
||||||
|
public override Color SeparatorLight => Background;
|
||||||
|
public override Color CheckBackground => Highlight;
|
||||||
|
public override Color CheckSelectedBackground => Highlight;
|
||||||
|
public override Color CheckPressedBackground => Highlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DarkMenuRenderer : ToolStripProfessionalRenderer
|
||||||
|
{
|
||||||
|
public DarkMenuRenderer() : base(new DarkMenuColors())
|
||||||
|
{
|
||||||
|
RoundedEdges = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
|
||||||
|
{
|
||||||
|
e.TextColor = e.Item.Enabled ? DarkMenuColors.Text : Color.FromArgb(0x80, 0x80, 0x80);
|
||||||
|
base.OnRenderItemText(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e)
|
||||||
|
{
|
||||||
|
e.ArrowColor = DarkMenuColors.Text;
|
||||||
|
base.OnRenderArrow(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e)
|
||||||
|
{
|
||||||
|
// Het standaardvinkje is zwart en valt weg op de donkere achtergrond.
|
||||||
|
Rectangle r = e.ImageRectangle;
|
||||||
|
using var background = new SolidBrush(DarkMenuColors.Highlight);
|
||||||
|
e.Graphics.FillRectangle(background, r);
|
||||||
|
using var pen = new Pen(DarkMenuColors.Text, 1.6f);
|
||||||
|
System.Drawing.Drawing2D.SmoothingMode smoothing = e.Graphics.SmoothingMode;
|
||||||
|
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
||||||
|
e.Graphics.DrawLines(pen, new[]
|
||||||
|
{
|
||||||
|
new PointF(r.Left + r.Width * 0.22f, r.Top + r.Height * 0.52f),
|
||||||
|
new PointF(r.Left + r.Width * 0.42f, r.Top + r.Height * 0.72f),
|
||||||
|
new PointF(r.Left + r.Width * 0.78f, r.Top + r.Height * 0.30f)
|
||||||
|
});
|
||||||
|
e.Graphics.SmoothingMode = smoothing;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,37 @@ internal static class NativeMethods
|
|||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
public static extern uint GetClipboardSequenceNumber();
|
public static extern uint GetClipboardSequenceNumber();
|
||||||
|
|
||||||
|
public const uint CF_UNICODETEXT = 13;
|
||||||
|
public const uint GMEM_MOVEABLE = 0x0002;
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool OpenClipboard(IntPtr hWndNewOwner);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool CloseClipboard();
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool EmptyClipboard();
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr SetClipboardData(uint format, IntPtr hMem);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr GlobalAlloc(uint flags, UIntPtr bytes);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr GlobalLock(IntPtr hMem);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool GlobalUnlock(IntPtr hMem);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr GlobalFree(IntPtr hMem);
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
||||||
|
|
||||||
@@ -193,6 +224,22 @@ internal static class NativeMethods
|
|||||||
[DllImport("dwmapi.dll")]
|
[DllImport("dwmapi.dll")]
|
||||||
private static extern int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out int value, int size);
|
private static extern int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out int value, int size);
|
||||||
|
|
||||||
|
[DllImport("dwmapi.dll")]
|
||||||
|
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size);
|
||||||
|
|
||||||
|
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
||||||
|
private const int DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19;
|
||||||
|
|
||||||
|
/// <summary>Donkere of lichte titelbalk; doet niets op Windows-versies die dat niet kennen.</summary>
|
||||||
|
public static void SetDarkTitleBar(IntPtr hwnd, bool dark)
|
||||||
|
{
|
||||||
|
int value = dark ? 1 : 0;
|
||||||
|
if (DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref value, sizeof(int)) != 0)
|
||||||
|
{
|
||||||
|
DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, ref value, sizeof(int));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void AddExtendedStyle(IntPtr hwnd, int style)
|
public static void AddExtendedStyle(IntPtr hwnd, int style)
|
||||||
{
|
{
|
||||||
long current = GetWindowLongPtr(hwnd, GWL_EXSTYLE).ToInt64();
|
long current = GetWindowLongPtr(hwnd, GWL_EXSTYLE).ToInt64();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<Product>kniip</Product>
|
<Product>kniip</Product>
|
||||||
<Description>Annotatie-editor naast het Windows Knipprogramma</Description>
|
<Description>Annotatie-editor naast het Windows Knipprogramma</Description>
|
||||||
<Version>1.1.1</Version>
|
<Version>1.1.7</Version>
|
||||||
<SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>nl;en</SatelliteResourceLanguages>
|
||||||
<!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. -->
|
<!-- WPF regelt DPI via app.manifest; de WinForms-analyzer is hier niet van toepassing. -->
|
||||||
<NoWarn>$(NoWarn);WFAC010</NoWarn>
|
<NoWarn>$(NoWarn);WFAC010</NoWarn>
|
||||||
@@ -26,8 +26,15 @@
|
|||||||
<AllowedReferenceRelatedFileExtensions>.none</AllowedReferenceRelatedFileExtensions>
|
<AllowedReferenceRelatedFileExtensions>.none</AllowedReferenceRelatedFileExtensions>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Testbuild (build.ps1, -p:KniipTest=true): eigen register- en instellingennamen, zie AppIdentity. -->
|
||||||
|
<PropertyGroup Condition="'$(KniipTest)' == 'true'">
|
||||||
|
<DefineConstants>$(DefineConstants);KNIIP_TEST</DefineConstants>
|
||||||
|
<Product>kniip test</Product>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Resource Include="AppIcon.ico" />
|
<Resource Include="AppIcon.ico" />
|
||||||
|
<Resource Include="TrayIcon.ico" />
|
||||||
<Resource Include="Assets\Emoji\emoji.json;Assets\Emoji\twemoji-64.png" />
|
<Resource Include="Assets\Emoji\emoji.json;Assets\Emoji\twemoji-64.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="kniip" WindowStyle="None" AllowsTransparency="True" Background="Transparent"
|
Title="kniip" WindowStyle="None" AllowsTransparency="True" Background="Transparent"
|
||||||
Topmost="True" ShowInTaskbar="False" ShowActivated="False" ResizeMode="NoResize"
|
Topmost="True" ShowInTaskbar="False" ShowActivated="False" ResizeMode="NoResize"
|
||||||
SizeToContent="WidthAndHeight" FontFamily="Segoe UI" UseLayoutRounding="True">
|
SizeToContent="WidthAndHeight" FontFamily="Segoe UI" Foreground="{DynamicResource InkBrush}" UseLayoutRounding="True">
|
||||||
<Border Margin="16" Background="#FCFCFC" CornerRadius="8" BorderBrush="#DADADA" BorderThickness="1">
|
<Border Margin="16" Background="{DynamicResource ToastBrush}" CornerRadius="8" BorderBrush="{DynamicResource ToastBorderBrush}" BorderThickness="1">
|
||||||
<Border.Effect>
|
<Border.Effect>
|
||||||
<DropShadowEffect BlurRadius="18" ShadowDepth="3" Opacity="0.22" />
|
<DropShadowEffect BlurRadius="18" ShadowDepth="3" Opacity="0.22" />
|
||||||
</Border.Effect>
|
</Border.Effect>
|
||||||
@@ -21,20 +21,20 @@
|
|||||||
|
|
||||||
<StackPanel Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,0,0,10">
|
<StackPanel Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,0,0,10">
|
||||||
<Image Source="pack://application:,,,/AppIcon.ico" Width="16" Height="16" />
|
<Image Source="pack://application:,,,/AppIcon.ico" Width="16" Height="16" />
|
||||||
<TextBlock Text="kniip" Margin="6,0,0,0" FontSize="12" Foreground="#5C5C5C" VerticalAlignment="Center" />
|
<TextBlock Text="kniip" Margin="6,0,0,0" FontSize="12" Foreground="{DynamicResource SubtleTextBrush}" VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Grid.Column="2" Style="{StaticResource FlatButton}" MinWidth="28" MinHeight="24" Padding="4,2"
|
<Button Grid.Column="2" Style="{StaticResource FlatButton}" MinWidth="28" MinHeight="24" Padding="4,2"
|
||||||
VerticalAlignment="Top" Click="Close_Click" ToolTip="Sluiten">
|
VerticalAlignment="Top" Click="Close_Click" ToolTip="Sluiten">
|
||||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="10" />
|
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="10" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Border Grid.Row="1" Width="110" Height="72" CornerRadius="4" BorderBrush="#E0E0E0" BorderThickness="1"
|
<Border Grid.Row="1" Width="110" Height="72" CornerRadius="4" BorderBrush="{DynamicResource ControlBorderBrush}" BorderThickness="1"
|
||||||
Background="#EBEBEB" Cursor="Hand" MouseLeftButtonUp="Thumbnail_Click" ClipToBounds="True">
|
Background="{DynamicResource ControlPressedBrush}" Cursor="Hand" MouseLeftButtonUp="Thumbnail_Click" ClipToBounds="True">
|
||||||
<Image x:Name="Thumbnail" Stretch="Uniform" RenderOptions.BitmapScalingMode="HighQuality" />
|
<Image x:Name="Thumbnail" Stretch="Uniform" RenderOptions.BitmapScalingMode="HighQuality" />
|
||||||
</Border>
|
</Border>
|
||||||
<StackPanel Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="12,0,4,0" VerticalAlignment="Center">
|
<StackPanel Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="12,0,4,0" VerticalAlignment="Center">
|
||||||
<TextBlock Text="Nieuw knipsel" FontWeight="SemiBold" FontSize="14" />
|
<TextBlock Text="Nieuw knipsel" FontWeight="SemiBold" FontSize="14" />
|
||||||
<TextBlock x:Name="DetailText" Foreground="#5C5C5C" FontSize="12" Margin="0,2,0,8" />
|
<TextBlock x:Name="DetailText" Foreground="{DynamicResource SubtleTextBrush}" FontSize="12" Margin="0,2,0,8" />
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Button Style="{StaticResource AccentButton}" Margin="0,0,6,0" Click="Edit_Click">
|
<Button Style="{StaticResource AccentButton}" Margin="0,0,6,0" Click="Edit_Click">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
|
|||||||
@@ -132,9 +132,13 @@ internal sealed class RecordingController
|
|||||||
NativeMethods.GetCursorPos(out NativeMethods.POINT cursor);
|
NativeMethods.GetCursorPos(out NativeMethods.POINT cursor);
|
||||||
var cursorPoint = new System.Drawing.Point(cursor.X, cursor.Y);
|
var cursorPoint = new System.Drawing.Point(cursor.X, cursor.Y);
|
||||||
|
|
||||||
foreach (Screen screen in Screen.AllScreens)
|
// Vastleggen voordat de overlays er staan, anders zit hun grijze laag in het vergrootglas.
|
||||||
|
var frozen = Screen.AllScreens
|
||||||
|
.Select(screen => (screen, image: _settings.ShowMagnifier ? Kniip.Capture.ScreenGrabber.Capture(screen.Bounds) : null))
|
||||||
|
.ToList();
|
||||||
|
foreach ((Screen screen, System.Windows.Media.Imaging.BitmapSource? image) in frozen)
|
||||||
{
|
{
|
||||||
var selector = new RegionSelectorWindow(screen, windows);
|
var selector = new RegionSelectorWindow(screen, windows, image);
|
||||||
selector.Finished += OnRegionChosen;
|
selector.Finished += OnRegionChosen;
|
||||||
_selectors.Add(selector);
|
_selectors.Add(selector);
|
||||||
selector.Show();
|
selector.Show();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ internal sealed record RecordingOptions(bool SystemAudio, bool Microphone, bool
|
|||||||
/// <summary>Eén opname naar een tijdelijk MP4-bestand. Gebeurtenissen komen binnen op de UI-thread.</summary>
|
/// <summary>Eén opname naar een tijdelijk MP4-bestand. Gebeurtenissen komen binnen op de UI-thread.</summary>
|
||||||
internal sealed class RecordingSession : IDisposable
|
internal sealed class RecordingSession : IDisposable
|
||||||
{
|
{
|
||||||
public static string TempFolder { get; } = Path.Combine(Path.GetTempPath(), "Kniip");
|
public static string TempFolder { get; } = Path.Combine(Path.GetTempPath(), Infrastructure.AppIdentity.Key);
|
||||||
|
|
||||||
private readonly Dispatcher _dispatcher;
|
private readonly Dispatcher _dispatcher;
|
||||||
private readonly RecordingRegion _region;
|
private readonly RecordingRegion _region;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="kniip opname" WindowStyle="None" AllowsTransparency="True" Background="Transparent"
|
Title="kniip opname" WindowStyle="None" AllowsTransparency="True" Background="Transparent"
|
||||||
ResizeMode="NoResize" SizeToContent="WidthAndHeight" ShowInTaskbar="False" Topmost="True"
|
ResizeMode="NoResize" SizeToContent="WidthAndHeight" ShowInTaskbar="False" Topmost="True"
|
||||||
FontFamily="Segoe UI" FontSize="13">
|
FontFamily="Segoe UI" FontSize="13" Foreground="{DynamicResource InkBrush}">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<Style x:Key="RecordStyle" TargetType="Button" BasedOn="{StaticResource AccentButton}">
|
<Style x:Key="RecordStyle" TargetType="Button" BasedOn="{StaticResource AccentButton}">
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
<Border Margin="10" Background="White" BorderBrush="#D6D6D6" BorderThickness="1" CornerRadius="8" Padding="6"
|
<Border Margin="10" Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource ControlStrongBorderBrush}" BorderThickness="1" CornerRadius="8" Padding="6"
|
||||||
MouseLeftButtonDown="Background_MouseLeftButtonDown">
|
MouseLeftButtonDown="Background_MouseLeftButtonDown">
|
||||||
<Border.Effect>
|
<Border.Effect>
|
||||||
<DropShadowEffect BlurRadius="14" ShadowDepth="2" Opacity="0.25" />
|
<DropShadowEffect BlurRadius="14" ShadowDepth="2" Opacity="0.25" />
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<StackPanel x:Name="TimePanel" Orientation="Horizontal" VerticalAlignment="Center" Margin="6,0,8,0" Visibility="Collapsed">
|
<StackPanel x:Name="TimePanel" Orientation="Horizontal" VerticalAlignment="Center" Margin="6,0,8,0" Visibility="Collapsed">
|
||||||
<Ellipse x:Name="RecordDot" Width="10" Height="10" Fill="#E81123" Margin="0,0,6,0" />
|
<Ellipse x:Name="RecordDot" Width="10" Height="10" Fill="#E81123" Margin="0,0,6,0" />
|
||||||
<TextBlock x:Name="TimeText" Text="0:00" FontWeight="SemiBold" MinWidth="40" Foreground="{StaticResource InkBrush}" />
|
<TextBlock x:Name="TimeText" Text="0:00" FontWeight="SemiBold" MinWidth="40" Foreground="{DynamicResource InkBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ToggleButton x:Name="SystemAudioToggle" Style="{StaticResource MiniToggle}" Height="34" MinWidth="34"
|
<ToggleButton x:Name="SystemAudioToggle" Style="{StaticResource MiniToggle}" Height="34" MinWidth="34"
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<TextBlock x:Name="BusyText" Text="Opname afronden..." VerticalAlignment="Center" Margin="8,0" Visibility="Collapsed"
|
<TextBlock x:Name="BusyText" Text="Opname afronden..." VerticalAlignment="Center" Margin="8,0" Visibility="Collapsed"
|
||||||
Foreground="{StaticResource SubtleTextBrush}" />
|
Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
|
|
||||||
<Button x:Name="CancelButton" Style="{StaticResource FlatButton}" Click="Cancel_Click" ToolTip="Annuleren (opname weggooien)">
|
<Button x:Name="CancelButton" Style="{StaticResource FlatButton}" Click="Cancel_Click" ToolTip="Annuleren (opname weggooien)">
|
||||||
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="14" />
|
<TextBlock Text="" Style="{StaticResource IconText}" FontSize="14" />
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
<Window x:Class="Kniip.Recording.RegionSelectorWindow"
|
<Window x:Class="Kniip.Recording.RegionSelectorWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:capture="clr-namespace:Kniip.Capture"
|
||||||
Title="kniip opnamegebied kiezen" WindowStyle="None" AllowsTransparency="True" ResizeMode="NoResize"
|
Title="kniip opnamegebied kiezen" WindowStyle="None" AllowsTransparency="True" ResizeMode="NoResize"
|
||||||
ShowInTaskbar="False" Topmost="True" Cursor="Cross" FontFamily="Segoe UI" FontSize="13"
|
ShowInTaskbar="False" Topmost="True" Cursor="Cross" FontFamily="Segoe UI" FontSize="13"
|
||||||
Background="#01000000">
|
Background="#01000000">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Path x:Name="Dim" Fill="#73000000" />
|
<Path x:Name="Dim" Fill="#73000000" />
|
||||||
<Rectangle x:Name="SelectionRect" Stroke="{StaticResource AccentBrush}" StrokeThickness="2"
|
<Rectangle x:Name="SelectionRect" Stroke="{DynamicResource AccentBrush}" StrokeThickness="2"
|
||||||
HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False" />
|
HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False" />
|
||||||
<Border x:Name="SizeBadge" HorizontalAlignment="Left" VerticalAlignment="Top" Background="#E6202020"
|
<Border x:Name="SizeBadge" HorizontalAlignment="Left" VerticalAlignment="Top" Background="#E6202020"
|
||||||
CornerRadius="4" Padding="6,2" Visibility="Collapsed" IsHitTestVisible="False">
|
CornerRadius="4" Padding="6,2" Visibility="Collapsed" IsHitTestVisible="False">
|
||||||
<TextBlock x:Name="SizeText" Foreground="White" FontSize="12" />
|
<TextBlock x:Name="SizeText" Foreground="White" FontSize="12" />
|
||||||
</Border>
|
</Border>
|
||||||
<Border x:Name="Hint" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,28,0,0" Background="#F2FFFFFF"
|
<capture:MagnifierControl x:Name="Magnifier" />
|
||||||
|
<Border x:Name="Hint" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,28,0,0" Background="{DynamicResource ToastBrush}"
|
||||||
CornerRadius="6" Padding="16,9" IsHitTestVisible="False">
|
CornerRadius="6" Padding="16,9" IsHitTestVisible="False">
|
||||||
<Border.Effect>
|
<Border.Effect>
|
||||||
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.25" />
|
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.25" />
|
||||||
</Border.Effect>
|
</Border.Effect>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Screencast: kies wat je wilt opnemen" FontWeight="SemiBold" Foreground="{StaticResource InkBrush}" />
|
<TextBlock Text="Screencast: kies wat je wilt opnemen" FontWeight="SemiBold" Foreground="{DynamicResource InkBrush}" />
|
||||||
<TextBlock Foreground="{StaticResource SubtleTextBrush}" FontSize="12" Margin="0,2,0,0"
|
<TextBlock Foreground="{DynamicResource SubtleTextBrush}" FontSize="12" Margin="0,2,0,0"
|
||||||
Text="Sleep een gebied · klik op een venster · Enter = heel scherm · Esc = annuleren" />
|
Text="Sleep een gebied · klik op een venster · Enter = heel scherm · Esc = annuleren" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
using Rectangle = System.Drawing.Rectangle;
|
using Rectangle = System.Drawing.Rectangle;
|
||||||
using Screen = System.Windows.Forms.Screen;
|
using Screen = System.Windows.Forms.Screen;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ public partial class RegionSelectorWindow : Window
|
|||||||
|
|
||||||
private readonly Screen _screen;
|
private readonly Screen _screen;
|
||||||
private readonly IReadOnlyList<Rectangle> _windows;
|
private readonly IReadOnlyList<Rectangle> _windows;
|
||||||
|
private readonly BitmapSource? _frozen;
|
||||||
private Point? _dragStart;
|
private Point? _dragStart;
|
||||||
private bool _dragging;
|
private bool _dragging;
|
||||||
private Rectangle? _hover;
|
private Rectangle? _hover;
|
||||||
@@ -26,15 +28,18 @@ public partial class RegionSelectorWindow : Window
|
|||||||
/// <summary>Gekozen gebied, of null bij annuleren.</summary>
|
/// <summary>Gekozen gebied, of null bij annuleren.</summary>
|
||||||
internal event Action<RecordingRegion?>? Finished;
|
internal event Action<RecordingRegion?>? Finished;
|
||||||
|
|
||||||
internal RegionSelectorWindow(Screen screen, IReadOnlyList<Rectangle> windows)
|
/// <param name="frozen">Beeld van het scherm voor het vergrootglas; null betekent geen vergrootglas.</param>
|
||||||
|
internal RegionSelectorWindow(Screen screen, IReadOnlyList<Rectangle> windows, BitmapSource? frozen = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_screen = screen;
|
_screen = screen;
|
||||||
_windows = windows;
|
_windows = windows;
|
||||||
|
_frozen = frozen;
|
||||||
SourceInitialized += (_, _) => PhysicalWindow.Place(this, screen.Bounds);
|
SourceInitialized += (_, _) => PhysicalWindow.Place(this, screen.Bounds);
|
||||||
Loaded += (_, _) => ShowSelection(null);
|
Loaded += (_, _) => ShowSelection(null);
|
||||||
MouseLeave += (_, _) =>
|
MouseLeave += (_, _) =>
|
||||||
{
|
{
|
||||||
|
Magnifier.Hide();
|
||||||
if (!_dragging)
|
if (!_dragging)
|
||||||
{
|
{
|
||||||
_hover = null;
|
_hover = null;
|
||||||
@@ -49,6 +54,13 @@ public partial class RegionSelectorWindow : Window
|
|||||||
{
|
{
|
||||||
base.OnMouseMove(e);
|
base.OnMouseMove(e);
|
||||||
Point position = e.GetPosition(this);
|
Point position = e.GetPosition(this);
|
||||||
|
if (_frozen != null)
|
||||||
|
{
|
||||||
|
System.Drawing.Point physical = ToPhysical(position);
|
||||||
|
var relative = new System.Drawing.Point(physical.X - _screen.Bounds.X, physical.Y - _screen.Bounds.Y);
|
||||||
|
Magnifier.Show(_frozen, relative, physical, position, new Size(ActualWidth, ActualHeight));
|
||||||
|
}
|
||||||
|
|
||||||
if (_dragStart is Point start && e.LeftButton == MouseButtonState.Pressed)
|
if (_dragStart is Point start && e.LeftButton == MouseButtonState.Pressed)
|
||||||
{
|
{
|
||||||
if (!_dragging && (position - start).Length >= DragThreshold)
|
if (!_dragging && (position - start).Length >= DragThreshold)
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="kniip screencast" Width="980" Height="720" MinWidth="680" MinHeight="500"
|
Title="kniip screencast" Width="980" Height="720" MinWidth="680" MinHeight="500"
|
||||||
WindowStartupLocation="CenterScreen" Background="{StaticResource WindowBrush}"
|
WindowStartupLocation="CenterScreen" Background="{DynamicResource WindowBrush}" Foreground="{DynamicResource InkBrush}"
|
||||||
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
|
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<Style x:Key="Handle" TargetType="Border">
|
<Style x:Key="Handle" TargetType="Border">
|
||||||
<Setter Property="Width" Value="12" />
|
<Setter Property="Width" Value="12" />
|
||||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
<Setter Property="Background" Value="{DynamicResource AccentBrush}" />
|
||||||
<Setter Property="Cursor" Value="SizeWE" />
|
<Setter Property="Cursor" Value="SizeWE" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style x:Key="Grip" TargetType="Rectangle">
|
<Style x:Key="Grip" TargetType="Rectangle">
|
||||||
@@ -43,10 +43,10 @@
|
|||||||
MouseLeftButtonDown="TrimArea_MouseLeftButtonDown" MouseMove="TrimArea_MouseMove"
|
MouseLeftButtonDown="TrimArea_MouseLeftButtonDown" MouseMove="TrimArea_MouseMove"
|
||||||
MouseLeftButtonUp="TrimArea_MouseLeftButtonUp" SizeChanged="TrimArea_SizeChanged">
|
MouseLeftButtonUp="TrimArea_MouseLeftButtonUp" SizeChanged="TrimArea_SizeChanged">
|
||||||
<Canvas x:Name="TrimCanvas">
|
<Canvas x:Name="TrimCanvas">
|
||||||
<Border x:Name="TrackBorder" Background="#DADADA" CornerRadius="5" Height="44" />
|
<Border x:Name="TrackBorder" Background="{DynamicResource TrackBrush}" CornerRadius="5" Height="44" />
|
||||||
<Border x:Name="SelectionBorder" Background="#330067C0" BorderBrush="{StaticResource AccentBrush}"
|
<Border x:Name="SelectionBorder" Background="#330067C0" BorderBrush="{DynamicResource AccentBrush}"
|
||||||
BorderThickness="0,2" Height="44" />
|
BorderThickness="0,2" Height="44" />
|
||||||
<Rectangle x:Name="Playhead" Width="2" Height="52" Fill="{StaticResource InkBrush}" Canvas.Top="-4" />
|
<Rectangle x:Name="Playhead" Width="2" Height="52" Fill="{DynamicResource InkBrush}" Canvas.Top="-4" />
|
||||||
<Border x:Name="StartHandle" Style="{StaticResource Handle}" CornerRadius="5,0,0,5" Height="44"
|
<Border x:Name="StartHandle" Style="{StaticResource Handle}" CornerRadius="5,0,0,5" Height="44"
|
||||||
ToolTip="Begin van de selectie">
|
ToolTip="Begin van de selectie">
|
||||||
<Rectangle Style="{StaticResource Grip}" />
|
<Rectangle Style="{StaticResource Grip}" />
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<TextBlock x:Name="PlayIcon" Text="" Style="{StaticResource IconText}" />
|
<TextBlock x:Name="PlayIcon" Text="" Style="{StaticResource IconText}" />
|
||||||
</Button>
|
</Button>
|
||||||
<TextBlock x:Name="PositionText" DockPanel.Dock="Left" VerticalAlignment="Center" Margin="6,0,0,0"
|
<TextBlock x:Name="PositionText" DockPanel.Dock="Left" VerticalAlignment="Center" Margin="6,0,0,0"
|
||||||
Foreground="{StaticResource InkBrush}" Text="0:00.0 / 0:00.0" />
|
Foreground="{DynamicResource InkBrush}" Text="0:00.0 / 0:00.0" />
|
||||||
|
|
||||||
<Button DockPanel.Dock="Right" Style="{StaticResource FlatButton}" Click="SetEnd_Click"
|
<Button DockPanel.Dock="Right" Style="{StaticResource FlatButton}" Click="SetEnd_Click"
|
||||||
ToolTip="Einde van de selectie op de huidige positie zetten">
|
ToolTip="Einde van de selectie op de huidige positie zetten">
|
||||||
@@ -81,10 +81,10 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<TextBlock x:Name="SelectionText" DockPanel.Dock="Right" VerticalAlignment="Center" Margin="0,0,10,0"
|
<TextBlock x:Name="SelectionText" DockPanel.Dock="Right" VerticalAlignment="Center" Margin="0,0,10,0"
|
||||||
Foreground="{StaticResource SubtleTextBrush}" />
|
Foreground="{DynamicResource SubtleTextBrush}" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<Border Grid.Row="3" Margin="0,12,0,0" Background="White" BorderBrush="#E5E5E5" BorderThickness="1"
|
<Border Grid.Row="3" Margin="0,12,0,0" Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource CardBorderBrush}" BorderThickness="1"
|
||||||
CornerRadius="6" Padding="14,10">
|
CornerRadius="6" Padding="14,10">
|
||||||
<DockPanel LastChildFill="True">
|
<DockPanel LastChildFill="True">
|
||||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock x:Name="InfoText" Style="{StaticResource PropLabel}" Margin="0,6,0,0" TextWrapping="Wrap" />
|
<TextBlock x:Name="InfoText" Style="{StaticResource PropLabel}" Margin="0,6,0,0" TextWrapping="Wrap" />
|
||||||
<TextBlock x:Name="SavedText" Margin="0,4,0,0" Visibility="Collapsed" FontSize="12" TextWrapping="Wrap">
|
<TextBlock x:Name="SavedText" Margin="0,4,0,0" Visibility="Collapsed" FontSize="12" TextWrapping="Wrap">
|
||||||
<Run x:Name="SavedRun" Foreground="{StaticResource InkBrush}" />
|
<Run x:Name="SavedRun" Foreground="{DynamicResource InkBrush}" />
|
||||||
<Hyperlink Click="OpenFolder_Click"><Run x:Name="OpenRun" Text="Map openen" /></Hyperlink>
|
<Hyperlink Click="OpenFolder_Click"><Run x:Name="OpenRun" Text="Map openen" /></Hyperlink>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public partial class ScreencastWindow : Window
|
|||||||
internal ScreencastWindow(string path, AppSettings settings, ScreencastFormat? format = null, string? windowName = null)
|
internal ScreencastWindow(string path, AppSettings settings, ScreencastFormat? format = null, string? windowName = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
Infrastructure.ThemeManager.Attach(this);
|
||||||
_path = path;
|
_path = path;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_windowName = windowName;
|
_windowName = windowName;
|
||||||
@@ -419,6 +420,7 @@ public partial class ScreencastWindow : Window
|
|||||||
{
|
{
|
||||||
_kept = true;
|
_kept = true;
|
||||||
RememberChoices();
|
RememberChoices();
|
||||||
|
AddToHistory(target, null, target);
|
||||||
bool copy = _settings.SaveButton == SaveButtonChoice.SaveAndCopy;
|
bool copy = _settings.SaveButton == SaveButtonChoice.SaveAndCopy;
|
||||||
bool copied = !copy || ClipboardWriter.SetFile(target);
|
bool copied = !copy || ClipboardWriter.SetFile(target);
|
||||||
App.Instance.ShowBalloon(copy && copied ? "Screencast opgeslagen en gekopieerd" : "Screencast opgeslagen", target);
|
App.Instance.ShowBalloon(copy && copied ? "Screencast opgeslagen en gekopieerd" : "Screencast opgeslagen", target);
|
||||||
@@ -524,6 +526,7 @@ public partial class ScreencastWindow : Window
|
|||||||
|
|
||||||
_kept = true;
|
_kept = true;
|
||||||
RememberChoices();
|
RememberChoices();
|
||||||
|
AddToHistory(null, url, target);
|
||||||
bool copied = ClipboardWriter.SetText(url);
|
bool copied = ClipboardWriter.SetText(url);
|
||||||
SavedRun.Text = copied ? $"Geüpload; de link staat op het klembord: {url} " : $"Geüpload: {url} ";
|
SavedRun.Text = copied ? $"Geüpload; de link staat op het klembord: {url} " : $"Geüpload: {url} ";
|
||||||
OpenRun.Text = "Link openen";
|
OpenRun.Text = "Link openen";
|
||||||
@@ -553,6 +556,14 @@ public partial class ScreencastWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <param name="exported">Het geëxporteerde bestand, voor de miniatuur van een GIF.</param>
|
||||||
|
private void AddToHistory(string? file, string? url, string exported)
|
||||||
|
{
|
||||||
|
bool gif = Format == ScreencastFormat.Gif;
|
||||||
|
Kniip.History.HistoryStore.Add(gif ? Kniip.History.HistoryKind.Gif : Kniip.History.HistoryKind.Mp4, file, url,
|
||||||
|
gif ? Kniip.History.HistoryStore.PreviewOf(exported) : null);
|
||||||
|
}
|
||||||
|
|
||||||
private void RememberChoices()
|
private void RememberChoices()
|
||||||
{
|
{
|
||||||
_settings.LastScreencastFormat = Format;
|
_settings.LastScreencastFormat = Format;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using Kniip.Editor;
|
using Kniip.Editor;
|
||||||
@@ -32,6 +33,24 @@ public enum SaveButtonChoice
|
|||||||
SaveAndCopy
|
SaveAndCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum EditorLayout
|
||||||
|
{
|
||||||
|
/// <summary>Eén werkbalk met gereedschappen en acties.</summary>
|
||||||
|
Single,
|
||||||
|
|
||||||
|
/// <summary>Split-weergave: gereedschappen boven, opslaan/kopiëren en zoom onder.</summary>
|
||||||
|
Split
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ColorMode
|
||||||
|
{
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
|
||||||
|
/// <summary>De app-modus van Windows volgen (licht of donker).</summary>
|
||||||
|
System
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class AppSettings
|
public sealed class AppSettings
|
||||||
{
|
{
|
||||||
public bool WatchClipboard { get; set; } = true;
|
public bool WatchClipboard { get; set; } = true;
|
||||||
@@ -63,6 +82,9 @@ public sealed class AppSettings
|
|||||||
|
|
||||||
/// <summary>Sneltoets voor een screencast die als GIF wordt aangeboden.</summary>
|
/// <summary>Sneltoets voor een screencast die als GIF wordt aangeboden.</summary>
|
||||||
public string GifHotkey { get; set; } = "Ctrl+Shift+G";
|
public string GifHotkey { get; set; } = "Ctrl+Shift+G";
|
||||||
|
|
||||||
|
/// <summary>Sneltoets om de afbeelding op het klembord in de editor te openen; standaard geen.</summary>
|
||||||
|
public string ClipboardHotkey { get; set; } = "";
|
||||||
public bool RecordSystemAudio { get; set; } = true;
|
public bool RecordSystemAudio { get; set; } = true;
|
||||||
public bool RecordMicrophone { get; set; }
|
public bool RecordMicrophone { get; set; }
|
||||||
public bool RecordShowCursor { get; set; } = true;
|
public bool RecordShowCursor { get; set; } = true;
|
||||||
@@ -91,11 +113,37 @@ public sealed class AppSettings
|
|||||||
/// <summary>Volgorde van de werkbalkitems (ItemId's); leeg betekent de standaardvolgorde.</summary>
|
/// <summary>Volgorde van de werkbalkitems (ItemId's); leeg betekent de standaardvolgorde.</summary>
|
||||||
public List<string> ToolbarOrder { get; set; } = new();
|
public List<string> ToolbarOrder { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Kleurmodus van alle vensters van kniip.</summary>
|
||||||
|
public ColorMode ColorMode { get; set; } = ColorMode.Light;
|
||||||
|
|
||||||
|
/// <summary>Indeling van de werkbalk(en) in de editor.</summary>
|
||||||
|
public EditorLayout EditorLayout { get; set; } = EditorLayout.Split;
|
||||||
|
|
||||||
|
/// <summary>Volgorde van de werkbalkitems in de gesplitste indeling (boven, dan onder); leeg betekent de standaardvolgorde.</summary>
|
||||||
|
public List<string> SplitToolbarOrder { get; set; } = new();
|
||||||
|
|
||||||
public List<string> ToolbarHidden { get; set; } = new();
|
public List<string> ToolbarHidden { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>Zipline-server waar Uploaden naartoe gaat; leeg betekent geen uploadknop.</summary>
|
/// <summary>Zipline-server waar Uploaden naartoe gaat; leeg betekent geen uploadknop.</summary>
|
||||||
public UploaderConfig? Uploader { get; set; }
|
public UploaderConfig? Uploader { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Bij het starten en daarna dagelijks op de achtergrond kijken of er een nieuwere versie is.</summary>
|
||||||
|
public bool CheckForUpdates { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Vergrootglas met kruisdraad en coördinaten tonen bij het kiezen van een gebied (knipsel en screencast).</summary>
|
||||||
|
public bool ShowMagnifier { get; set; } = true;
|
||||||
|
|
||||||
|
private int _captureDelaySeconds;
|
||||||
|
|
||||||
|
/// <summary>Seconden wachten voordat een rechthoek- of vensterknipsel het scherm bevriest (0-60); 0 is direct.</summary>
|
||||||
|
public int CaptureDelaySeconds
|
||||||
|
{
|
||||||
|
get => _captureDelaySeconds;
|
||||||
|
set => _captureDelaySeconds = Math.Clamp(value, 0, MaxCaptureDelaySeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public const int MaxCaptureDelaySeconds = 60;
|
||||||
|
|
||||||
public StyleSet StyleFor(ToolKind tool)
|
public StyleSet StyleFor(ToolKind tool)
|
||||||
{
|
{
|
||||||
if (!ToolStyles.TryGetValue(tool, out StyleSet? style))
|
if (!ToolStyles.TryGetValue(tool, out StyleSet? style))
|
||||||
|
|||||||
@@ -7,19 +7,24 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using Kniip.Infrastructure;
|
||||||
|
|
||||||
namespace Kniip.Settings;
|
namespace Kniip.Settings;
|
||||||
|
|
||||||
public static class SettingsStore
|
public static class SettingsStore
|
||||||
{
|
{
|
||||||
public static string Folder { get; } =
|
public static string Folder { get; } =
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Kniip");
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppIdentity.Key);
|
||||||
|
|
||||||
private static string FilePath => Path.Combine(Folder, "settings.json");
|
private static string FilePath => Path.Combine(Folder, "settings.json");
|
||||||
|
|
||||||
/// <summary>Instellingen van voor de hernoeming van Knip+ naar kniip.</summary>
|
/// <summary>
|
||||||
|
/// Startpunt als er nog geen instellingen zijn: voor kniip die van voor de hernoeming van Knip+,
|
||||||
|
/// voor de testbuild een kopie van die van de gewone kniip.
|
||||||
|
/// </summary>
|
||||||
private static string LegacyFilePath =>
|
private static string LegacyFilePath =>
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "KnipPlus", "settings.json");
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
AppIdentity.IsTest ? "Kniip" : "KnipPlus", "settings.json");
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions Options = new()
|
private static readonly JsonSerializerOptions Options = new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,28 +2,35 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="kniip instellingen" Width="720" Height="600" ResizeMode="NoResize"
|
Title="kniip instellingen" Width="720" Height="600" ResizeMode="NoResize"
|
||||||
WindowStartupLocation="CenterScreen" Background="{StaticResource WindowBrush}"
|
WindowStartupLocation="CenterScreen" Background="{DynamicResource WindowBrush}" Foreground="{DynamicResource InkBrush}"
|
||||||
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
|
FontFamily="Segoe UI" FontSize="13" Icon="pack://application:,,,/AppIcon.ico">
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
|
<!-- Inhoud van een tab erft anders de blauwe, vette tekst van de geselecteerde tabkop -->
|
||||||
|
<Style x:Key="TabPage" TargetType="StackPanel">
|
||||||
|
<Setter Property="TextElement.Foreground" Value="{DynamicResource InkBrush}" />
|
||||||
|
<Setter Property="TextElement.FontWeight" Value="Normal" />
|
||||||
|
</Style>
|
||||||
<Style x:Key="CardTitle" TargetType="TextBlock">
|
<Style x:Key="CardTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentTextBrush}" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
<Setter Property="FontSize" Value="14" />
|
<Setter Property="FontSize" Value="14" />
|
||||||
<Setter Property="Margin" Value="0,0,0,8" />
|
<Setter Property="Margin" Value="0,0,0,8" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="CheckBox">
|
<Style TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
|
||||||
<Setter Property="Margin" Value="0,4" />
|
<Setter Property="Margin" Value="0,4" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="RadioButton">
|
<Style TargetType="RadioButton" BasedOn="{StaticResource {x:Type RadioButton}}">
|
||||||
<Setter Property="Margin" Value="0,4" />
|
<Setter Property="Margin" Value="0,4" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style x:Key="Hint" TargetType="TextBlock">
|
<Style x:Key="Hint" TargetType="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{StaticResource SubtleTextBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource SubtleTextBrush}" />
|
||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
<Setter Property="TextWrapping" Value="Wrap" />
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
<Setter Property="Margin" Value="22,0,0,4" />
|
<Setter Property="Margin" Value="22,0,0,4" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style x:Key="SettingsTabItem" TargetType="TabItem">
|
<Style x:Key="SettingsTabItem" TargetType="TabItem">
|
||||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource InkBrush}" />
|
||||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||||
<Setter Property="Padding" Value="12,9" />
|
<Setter Property="Padding" Value="12,9" />
|
||||||
@@ -42,12 +49,12 @@
|
|||||||
<Condition Property="IsMouseOver" Value="True" />
|
<Condition Property="IsMouseOver" Value="True" />
|
||||||
<Condition Property="IsSelected" Value="False" />
|
<Condition Property="IsSelected" Value="False" />
|
||||||
</MultiTrigger.Conditions>
|
</MultiTrigger.Conditions>
|
||||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource HoverBrush}" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource HoverBrush}" />
|
||||||
</MultiTrigger>
|
</MultiTrigger>
|
||||||
<Trigger Property="IsSelected" Value="True">
|
<Trigger Property="IsSelected" Value="True">
|
||||||
<Setter TargetName="Bd" Property="Background" Value="White" />
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource CardBrush}" />
|
||||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource AccentBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource AccentTextBrush}" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</ControlTemplate.Triggers>
|
</ControlTemplate.Triggers>
|
||||||
@@ -69,7 +76,7 @@
|
|||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TabPanel Grid.Column="0" IsItemsHost="True" VerticalAlignment="Top" Background="Transparent" />
|
<TabPanel Grid.Column="0" IsItemsHost="True" VerticalAlignment="Top" Background="Transparent" />
|
||||||
<Border Grid.Column="1" Background="White" BorderBrush="#E5E5E5" BorderThickness="1"
|
<Border Grid.Column="1" Background="{DynamicResource CardBrush}" BorderBrush="{DynamicResource CardBorderBrush}" BorderThickness="1"
|
||||||
CornerRadius="6" SnapsToDevicePixels="True" Padding="16,14">
|
CornerRadius="6" SnapsToDevicePixels="True" Padding="16,14">
|
||||||
<ContentPresenter ContentSource="SelectedContent" />
|
<ContentPresenter ContentSource="SelectedContent" />
|
||||||
</Border>
|
</Border>
|
||||||
@@ -93,18 +100,8 @@
|
|||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<TabControl Style="{StaticResource SettingsTabs}" SelectionChanged="Tabs_SelectionChanged">
|
<TabControl Style="{StaticResource SettingsTabs}" SelectionChanged="Tabs_SelectionChanged">
|
||||||
<TabItem Header="Knipsels">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="Knipsels oppikken" Style="{StaticResource CardTitle}" />
|
|
||||||
<CheckBox x:Name="WatchClipboardBox" Content="Klembord bewaken" />
|
|
||||||
<CheckBox x:Name="OnlySnippingToolBox" Content="Alleen afbeeldingen van het Knipprogramma" Margin="22,2,0,4" />
|
|
||||||
<CheckBox x:Name="WatchFolderBox" Content="Map Schermafbeeldingen bewaken" />
|
|
||||||
<TextBlock x:Name="FolderText" Style="{StaticResource Hint}" />
|
|
||||||
</StackPanel>
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem Header="Sneltoetsen">
|
<TabItem Header="Sneltoetsen">
|
||||||
<StackPanel>
|
<StackPanel Style="{StaticResource TabPage}">
|
||||||
<TextBlock Text="Sneltoetsen" Style="{StaticResource CardTitle}" />
|
<TextBlock Text="Sneltoetsen" Style="{StaticResource CardTitle}" />
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,2,0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,2,0,4">
|
||||||
<TextBlock Text="Rechthoek" VerticalAlignment="Center" Width="150" />
|
<TextBlock Text="Rechthoek" VerticalAlignment="Center" Width="150" />
|
||||||
@@ -127,30 +124,28 @@
|
|||||||
<Button Tag="Record" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
|
<Button Tag="Record" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
|
||||||
ToolTip="Geen sneltoets gebruiken" />
|
ToolTip="Geen sneltoets gebruiken" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||||
<TextBlock Text="Screencast (GIF)" VerticalAlignment="Center" Width="150" />
|
<TextBlock Text="Screencast (GIF)" VerticalAlignment="Center" Width="150" />
|
||||||
<Button x:Name="GifHotkeyButton" Tag="Gif" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
|
<Button x:Name="GifHotkeyButton" Tag="Gif" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
|
||||||
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in" />
|
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in" />
|
||||||
<Button Tag="Gif" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
|
<Button Tag="Gif" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
|
||||||
ToolTip="Geen sneltoets gebruiken" />
|
ToolTip="Geen sneltoets gebruiken" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock x:Name="HotkeyHint" Style="{StaticResource Hint}" Margin="0,0,0,4" />
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||||
|
<TextBlock Text="Vanaf klembord" VerticalAlignment="Center" Width="150" />
|
||||||
|
<Button x:Name="ClipboardHotkeyButton" Tag="Clipboard" Style="{StaticResource SecondaryButton}" Width="170" Margin="0"
|
||||||
|
Click="HotkeyButton_Click" ToolTip="Klik en druk daarna de gewenste toetscombinatie in; opent de afbeelding op het klembord" />
|
||||||
|
<Button Tag="Clipboard" Style="{StaticResource FlatButton}" Content="Geen" Click="ClearHotkey_Click" Margin="6,0,0,0"
|
||||||
|
ToolTip="Geen sneltoets gebruiken" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TabItem>
|
<TextBlock x:Name="HotkeyHint" Style="{StaticResource Hint}" Margin="0,0,0,4" />
|
||||||
|
<TextBlock Text="Gebied kiezen" Style="{StaticResource CardTitle}" Margin="0,12,0,8" />
|
||||||
<TabItem Header="Nieuw knipsel">
|
<CheckBox x:Name="MagnifierBox" Content="Vergrootglas tonen bij het kiezen van een gebied (knipsel en screencast)" />
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="Bij een nieuw knipsel" Style="{StaticResource CardTitle}" />
|
|
||||||
<RadioButton x:Name="ActionNotifyRadio" GroupName="Action" Content="Melding tonen (klik om te bewerken)" />
|
|
||||||
<RadioButton x:Name="ActionOpenRadio" GroupName="Action" Content="Editor meteen openen" />
|
|
||||||
<RadioButton x:Name="ActionNothingRadio" GroupName="Action" Content="Niets doen (openen via systeemvak of 'Openen met')" />
|
|
||||||
<TextBlock Style="{StaticResource Hint}" Margin="0,8,0,0"
|
|
||||||
Text="Tip: zet in het Knipprogramma 'Wijzigingen automatisch kopiëren' uit, anders ziet kniip elke pennenstreek in het Knipprogramma als nieuw knipsel." />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="Opslaan">
|
<TabItem Header="Opslaan">
|
||||||
<StackPanel>
|
<StackPanel Style="{StaticResource TabPage}">
|
||||||
<TextBlock Text="Opslaan" Style="{StaticResource CardTitle}" />
|
<TextBlock Text="Opslaan" Style="{StaticResource CardTitle}" />
|
||||||
<DockPanel Margin="0,2,0,4">
|
<DockPanel Margin="0,2,0,4">
|
||||||
<TextBlock Text="Knipsels" VerticalAlignment="Center" Width="110" />
|
<TextBlock Text="Knipsels" VerticalAlignment="Center" Width="110" />
|
||||||
@@ -191,7 +186,7 @@
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="Uploaden">
|
<TabItem Header="Uploaden">
|
||||||
<StackPanel>
|
<StackPanel Style="{StaticResource TabPage}">
|
||||||
<TextBlock Text="Uploaden naar Zipline" Style="{StaticResource CardTitle}" />
|
<TextBlock Text="Uploaden naar Zipline" Style="{StaticResource CardTitle}" />
|
||||||
<DockPanel Margin="0,2,0,4">
|
<DockPanel Margin="0,2,0,4">
|
||||||
<TextBlock Text="Server" VerticalAlignment="Center" Width="110" />
|
<TextBlock Text="Server" VerticalAlignment="Center" Width="110" />
|
||||||
@@ -212,7 +207,7 @@
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="Screencasts">
|
<TabItem Header="Screencasts">
|
||||||
<StackPanel>
|
<StackPanel Style="{StaticResource TabPage}">
|
||||||
<TextBlock Text="Screencasts" Style="{StaticResource CardTitle}" />
|
<TextBlock Text="Screencasts" Style="{StaticResource CardTitle}" />
|
||||||
<TextBlock Style="{StaticResource Hint}" Margin="0,0,0,6"
|
<TextBlock Style="{StaticResource Hint}" Margin="0,0,0,6"
|
||||||
Text="Sneltoets: eerste keer gebied kiezen, tweede keer opname starten, derde keer stoppen. De nabewerking staat daarna op MP4 of GIF." />
|
Text="Sneltoets: eerste keer gebied kiezen, tweede keer opname starten, derde keer stoppen. De nabewerking staat daarna op MP4 of GIF." />
|
||||||
@@ -232,9 +227,40 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="Windows">
|
<TabItem Header="Weergave">
|
||||||
<StackPanel>
|
<StackPanel Style="{StaticResource TabPage}">
|
||||||
<TextBlock Text="Windows-integratie" Style="{StaticResource CardTitle}" />
|
<TextBlock Text="Kleurmodus" Style="{StaticResource CardTitle}" />
|
||||||
|
<RadioButton x:Name="ColorModeLightRadio" GroupName="ColorMode" Content="Licht" />
|
||||||
|
<RadioButton x:Name="ColorModeDarkRadio" GroupName="ColorMode" Content="Donker" />
|
||||||
|
<RadioButton x:Name="ColorModeSystemRadio" GroupName="ColorMode" Content="Systeem" />
|
||||||
|
<TextBlock Style="{StaticResource Hint}"
|
||||||
|
Text="Systeem volgt de app-modus van Windows (Instellingen > Persoonlijke instellingen > Kleuren) en wisselt mee als je die verandert." />
|
||||||
|
<TextBlock Text="Editor" Style="{StaticResource CardTitle}" Margin="0,16,0,6" />
|
||||||
|
<CheckBox x:Name="StraightHighlightBox" Content="Markeerstift trekt standaard een rechte horizontale streep (Shift wisselt)" />
|
||||||
|
<TextBlock Text="Werkbalk" Style="{StaticResource CardTitle}" Margin="0,16,0,6" />
|
||||||
|
<RadioButton x:Name="LayoutSplitRadio" GroupName="EditorLayout" Content="Split-weergave: gereedschappen boven, opslaan onder" />
|
||||||
|
<RadioButton x:Name="LayoutSingleRadio" GroupName="EditorLayout" Content="Eén werkbalk met gereedschappen en acties" />
|
||||||
|
<TextBlock Style="{StaticResource Hint}"
|
||||||
|
Text="Ook te wisselen via rechtsklik op de werkbalk in de editor. Daar kun je knoppen ook verslepen of verbergen." />
|
||||||
|
</StackPanel>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="Systeem">
|
||||||
|
<StackPanel Style="{StaticResource TabPage}">
|
||||||
|
<TextBlock Text="Knipprogramma" Style="{StaticResource CardTitle}" />
|
||||||
|
<CheckBox x:Name="WatchClipboardBox" Content="Klembord bewaken" />
|
||||||
|
<CheckBox x:Name="OnlySnippingToolBox" Content="Alleen afbeeldingen van het Knipprogramma" Margin="22,2,0,4" />
|
||||||
|
<CheckBox x:Name="WatchFolderBox" Content="Map Schermafbeeldingen bewaken" />
|
||||||
|
<TextBlock x:Name="FolderText" Style="{StaticResource Hint}" />
|
||||||
|
|
||||||
|
<TextBlock Text="Bij een nieuw knipsel" Style="{StaticResource CardTitle}" Margin="0,16,0,6" />
|
||||||
|
<RadioButton x:Name="ActionNotifyRadio" GroupName="Action" Content="Melding tonen (klik om te bewerken)" />
|
||||||
|
<RadioButton x:Name="ActionOpenRadio" GroupName="Action" Content="Editor meteen openen" />
|
||||||
|
<RadioButton x:Name="ActionNothingRadio" GroupName="Action" Content="Niets doen (openen via systeemvak of 'Openen met')" />
|
||||||
|
<TextBlock Style="{StaticResource Hint}" Margin="0,8,0,0"
|
||||||
|
Text="Tip: zet in het Knipprogramma 'Wijzigingen automatisch kopiëren' uit, anders ziet kniip elke pennenstreek in het Knipprogramma als nieuw knipsel." />
|
||||||
|
|
||||||
|
<TextBlock Text="Windows-integratie" Style="{StaticResource CardTitle}" Margin="0,16,0,6" />
|
||||||
<CheckBox x:Name="AutostartBox" Content="kniip starten met Windows" />
|
<CheckBox x:Name="AutostartBox" Content="kniip starten met Windows" />
|
||||||
<CheckBox x:Name="OpenWithBox" Content="kniip tonen in 'Openen met' (ook in het Knipprogramma: ... → Openen met)" />
|
<CheckBox x:Name="OpenWithBox" Content="kniip tonen in 'Openen met' (ook in het Knipprogramma: ... → Openen met)" />
|
||||||
<TextBlock Style="{StaticResource Hint}"
|
<TextBlock Style="{StaticResource Hint}"
|
||||||
@@ -242,17 +268,19 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="Editor">
|
<TabItem x:Name="UpdateTab">
|
||||||
<StackPanel>
|
<TabItem.Header>
|
||||||
<TextBlock Text="Editor" Style="{StaticResource CardTitle}" />
|
<StackPanel Orientation="Horizontal">
|
||||||
<CheckBox x:Name="StraightHighlightBox" Content="Markeerstift trekt standaard een rechte horizontale streep (Shift wisselt)" />
|
<TextBlock Text="Update" />
|
||||||
|
<Ellipse x:Name="UpdateTabBadge" Width="8" Height="8" Fill="{DynamicResource AccentBrush}" Stroke="{DynamicResource WindowBrush}"
|
||||||
|
StrokeThickness="1" Margin="6,0,0,0" VerticalAlignment="Center" Visibility="Collapsed"
|
||||||
|
ToolTip="Er is een nieuwe versie beschikbaar" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TabItem>
|
</TabItem.Header>
|
||||||
|
<StackPanel Style="{StaticResource TabPage}">
|
||||||
<TabItem x:Name="UpdateTab" Header="Update">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Text="Update" Style="{StaticResource CardTitle}" />
|
<TextBlock Text="Update" Style="{StaticResource CardTitle}" />
|
||||||
<TextBlock x:Name="CurrentVersionText" Margin="0,0,0,2" />
|
<TextBlock x:Name="CurrentVersionText" Margin="0,0,0,2" />
|
||||||
|
<CheckBox x:Name="AutoUpdateCheckBox" Content="Automatisch controleren op nieuwe versies (bij starten en dagelijks)" />
|
||||||
<TextBlock x:Name="UpdateStatus" Style="{StaticResource Hint}" Margin="0,0,0,8" />
|
<TextBlock x:Name="UpdateStatus" Style="{StaticResource Hint}" Margin="0,0,0,8" />
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
<Button x:Name="UpdateButton" Style="{StaticResource AccentButton}" Click="Update_Click" Margin="0,0,6,0"
|
<Button x:Name="UpdateButton" Style="{StaticResource AccentButton}" Click="Update_Click" Margin="0,0,6,0"
|
||||||
@@ -263,8 +291,8 @@
|
|||||||
ToolTip="https://git.hibbit.net/rogier/kniip/releases" />
|
ToolTip="https://git.hibbit.net/rogier/kniip/releases" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ProgressBar x:Name="DownloadProgress" Height="4" Minimum="0" Maximum="1" Margin="0,0,0,8"
|
<ProgressBar x:Name="DownloadProgress" Height="4" Minimum="0" Maximum="1" Margin="0,0,0,8"
|
||||||
Foreground="{StaticResource AccentBrush}" Visibility="Collapsed" />
|
Foreground="{DynamicResource AccentBrush}" Visibility="Collapsed" />
|
||||||
<Border BorderBrush="{StaticResource DividerBrush}" BorderThickness="1" CornerRadius="4" Height="300">
|
<Border BorderBrush="{DynamicResource DividerBrush}" BorderThickness="1" CornerRadius="4" Height="300">
|
||||||
<FlowDocumentScrollViewer x:Name="ChangelogViewer" VerticalScrollBarVisibility="Auto"
|
<FlowDocumentScrollViewer x:Name="ChangelogViewer" VerticalScrollBarVisibility="Auto"
|
||||||
HorizontalScrollBarVisibility="Disabled" IsToolBarVisible="False"
|
HorizontalScrollBarVisibility="Disabled" IsToolBarVisible="False"
|
||||||
Zoom="100" MinZoom="100" MaxZoom="100" Focusable="False" />
|
Zoom="100" MinZoom="100" MaxZoom="100" Focusable="False" />
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public partial class SettingsWindow : Window
|
|||||||
["Window"] = "venster knippen",
|
["Window"] = "venster knippen",
|
||||||
["Record"] = "screencast (MP4)",
|
["Record"] = "screencast (MP4)",
|
||||||
["Gif"] = "screencast (GIF)",
|
["Gif"] = "screencast (GIF)",
|
||||||
|
["Clipboard"] = "bewerken vanaf klembord",
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly AppSettings _settings;
|
private readonly AppSettings _settings;
|
||||||
@@ -43,7 +44,9 @@ public partial class SettingsWindow : Window
|
|||||||
public SettingsWindow(AppSettings settings)
|
public SettingsWindow(AppSettings settings)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
ThemeManager.Attach(this);
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
Title = $"{AppIdentity.DisplayName} instellingen";
|
||||||
|
|
||||||
FolderText.Text = CaptureCoordinator.ScreenshotsFolder ?? "Map Schermafbeeldingen niet gevonden.";
|
FolderText.Text = CaptureCoordinator.ScreenshotsFolder ?? "Map Schermafbeeldingen niet gevonden.";
|
||||||
AutostartBox.IsChecked = ShellIntegration.IsAutostartEnabled();
|
AutostartBox.IsChecked = ShellIntegration.IsAutostartEnabled();
|
||||||
@@ -62,17 +65,21 @@ public partial class SettingsWindow : Window
|
|||||||
["Window"] = WindowHotkeyButton,
|
["Window"] = WindowHotkeyButton,
|
||||||
["Record"] = RecordHotkeyButton,
|
["Record"] = RecordHotkeyButton,
|
||||||
["Gif"] = GifHotkeyButton,
|
["Gif"] = GifHotkeyButton,
|
||||||
|
["Clipboard"] = ClipboardHotkeyButton,
|
||||||
};
|
};
|
||||||
|
|
||||||
HotkeyHint.Text = App.Instance.HotkeyHook?.IsActive == true
|
HotkeyHint.Text = App.Instance.HotkeyHook?.IsActive == true
|
||||||
? HotkeyHintText
|
? HotkeyHintText
|
||||||
: "De toetsenbordhaak kon niet worden gestart; de sneltoetsen werken nu niet.";
|
: "De toetsenbordhaak kon niet worden gestart; de sneltoetsen werken nu niet.";
|
||||||
Deactivated += (_, _) => StopListening();
|
Deactivated += (_, _) => StopListening();
|
||||||
|
App.Instance.Updates.AvailabilityChanged += OnUpdateAvailabilityChanged;
|
||||||
Closed += (_, _) =>
|
Closed += (_, _) =>
|
||||||
{
|
{
|
||||||
StopListening();
|
StopListening();
|
||||||
_updateCts?.Cancel();
|
_updateCts?.Cancel();
|
||||||
|
App.Instance.Updates.AvailabilityChanged -= OnUpdateAvailabilityChanged;
|
||||||
};
|
};
|
||||||
|
ShowUpdateBadge();
|
||||||
|
|
||||||
CurrentVersionText.Text = $"Geïnstalleerde versie: {UpdateChecker.CurrentVersion}";
|
CurrentVersionText.Text = $"Geïnstalleerde versie: {UpdateChecker.CurrentVersion}";
|
||||||
LoadFrom(settings);
|
LoadFrom(settings);
|
||||||
@@ -91,6 +98,13 @@ public partial class SettingsWindow : Window
|
|||||||
ActionOpenRadio.IsChecked = settings.Action == CaptureAction.OpenEditor;
|
ActionOpenRadio.IsChecked = settings.Action == CaptureAction.OpenEditor;
|
||||||
ActionNothingRadio.IsChecked = settings.Action == CaptureAction.Nothing;
|
ActionNothingRadio.IsChecked = settings.Action == CaptureAction.Nothing;
|
||||||
StraightHighlightBox.IsChecked = settings.StraightHighlight;
|
StraightHighlightBox.IsChecked = settings.StraightHighlight;
|
||||||
|
ColorModeLightRadio.IsChecked = settings.ColorMode == ColorMode.Light;
|
||||||
|
ColorModeDarkRadio.IsChecked = settings.ColorMode == ColorMode.Dark;
|
||||||
|
ColorModeSystemRadio.IsChecked = settings.ColorMode == ColorMode.System;
|
||||||
|
LayoutSplitRadio.IsChecked = settings.EditorLayout == EditorLayout.Split;
|
||||||
|
LayoutSingleRadio.IsChecked = settings.EditorLayout != EditorLayout.Split;
|
||||||
|
AutoUpdateCheckBox.IsChecked = settings.CheckForUpdates;
|
||||||
|
MagnifierBox.IsChecked = settings.ShowMagnifier;
|
||||||
|
|
||||||
_snipFolder = settings.SnipSaveFolder;
|
_snipFolder = settings.SnipSaveFolder;
|
||||||
_videoFolder = settings.VideoSaveFolder;
|
_videoFolder = settings.VideoSaveFolder;
|
||||||
@@ -105,6 +119,7 @@ public partial class SettingsWindow : Window
|
|||||||
_hotkeys["Window"] = settings.WindowHotkey;
|
_hotkeys["Window"] = settings.WindowHotkey;
|
||||||
_hotkeys["Record"] = settings.RecordHotkey;
|
_hotkeys["Record"] = settings.RecordHotkey;
|
||||||
_hotkeys["Gif"] = settings.GifHotkey;
|
_hotkeys["Gif"] = settings.GifHotkey;
|
||||||
|
_hotkeys["Clipboard"] = settings.ClipboardHotkey;
|
||||||
foreach (string id in _hotkeys.Keys)
|
foreach (string id in _hotkeys.Keys)
|
||||||
{
|
{
|
||||||
ShowHotkey(id);
|
ShowHotkey(id);
|
||||||
@@ -242,7 +257,7 @@ public partial class SettingsWindow : Window
|
|||||||
{
|
{
|
||||||
text.Text = string.IsNullOrEmpty(folder) ? "Elke keer vragen" : folder;
|
text.Text = string.IsNullOrEmpty(folder) ? "Elke keer vragen" : folder;
|
||||||
text.ToolTip = folder;
|
text.ToolTip = folder;
|
||||||
text.Foreground = string.IsNullOrEmpty(folder) ? (Brush)Application.Current.Resources["SubtleTextBrush"] : SystemColors.ControlTextBrush;
|
text.SetResourceReference(TextBlock.ForegroundProperty, string.IsNullOrEmpty(folder) ? "SubtleTextBrush" : "InkBrush");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Template_TextChanged(object sender, TextChangedEventArgs e) => ShowTemplatePreviews();
|
private void Template_TextChanged(object sender, TextChangedEventArgs e) => ShowTemplatePreviews();
|
||||||
@@ -305,7 +320,7 @@ public partial class SettingsWindow : Window
|
|||||||
bool configured = _uploader is { IsConfigured: true };
|
bool configured = _uploader is { IsConfigured: true };
|
||||||
UploaderText.Text = configured ? _uploader!.DisplayName : "Niet ingesteld";
|
UploaderText.Text = configured ? _uploader!.DisplayName : "Niet ingesteld";
|
||||||
UploaderText.ToolTip = configured ? _uploader!.RequestUrl : null;
|
UploaderText.ToolTip = configured ? _uploader!.RequestUrl : null;
|
||||||
UploaderText.Foreground = configured ? SystemColors.ControlTextBrush : (Brush)Application.Current.Resources["SubtleTextBrush"];
|
UploaderText.SetResourceReference(TextBlock.ForegroundProperty, configured ? "InkBrush" : "SubtleTextBrush");
|
||||||
TestUploaderButton.IsEnabled = configured;
|
TestUploaderButton.IsEnabled = configured;
|
||||||
RemoveUploaderButton.IsEnabled = configured;
|
RemoveUploaderButton.IsEnabled = configured;
|
||||||
}
|
}
|
||||||
@@ -433,11 +448,18 @@ public partial class SettingsWindow : Window
|
|||||||
: ActionNothingRadio.IsChecked == true ? CaptureAction.Nothing
|
: ActionNothingRadio.IsChecked == true ? CaptureAction.Nothing
|
||||||
: CaptureAction.Notify;
|
: CaptureAction.Notify;
|
||||||
_settings.StraightHighlight = StraightHighlightBox.IsChecked == true;
|
_settings.StraightHighlight = StraightHighlightBox.IsChecked == true;
|
||||||
|
_settings.ColorMode = ColorModeDarkRadio.IsChecked == true ? ColorMode.Dark
|
||||||
|
: ColorModeSystemRadio.IsChecked == true ? ColorMode.System
|
||||||
|
: ColorMode.Light;
|
||||||
|
_settings.EditorLayout = LayoutSplitRadio.IsChecked == true ? EditorLayout.Split : EditorLayout.Single;
|
||||||
|
_settings.CheckForUpdates = AutoUpdateCheckBox.IsChecked == true;
|
||||||
|
_settings.ShowMagnifier = MagnifierBox.IsChecked == true;
|
||||||
StopListening();
|
StopListening();
|
||||||
_settings.SnipHotkey = _hotkeys["Snip"];
|
_settings.SnipHotkey = _hotkeys["Snip"];
|
||||||
_settings.WindowHotkey = _hotkeys["Window"];
|
_settings.WindowHotkey = _hotkeys["Window"];
|
||||||
_settings.RecordHotkey = _hotkeys["Record"];
|
_settings.RecordHotkey = _hotkeys["Record"];
|
||||||
_settings.GifHotkey = _hotkeys["Gif"];
|
_settings.GifHotkey = _hotkeys["Gif"];
|
||||||
|
_settings.ClipboardHotkey = _hotkeys["Clipboard"];
|
||||||
_settings.RecordSystemAudio = SystemAudioBox.IsChecked == true;
|
_settings.RecordSystemAudio = SystemAudioBox.IsChecked == true;
|
||||||
_settings.RecordMicrophone = MicrophoneBox.IsChecked == true;
|
_settings.RecordMicrophone = MicrophoneBox.IsChecked == true;
|
||||||
_settings.RecordShowCursor = ShowCursorBox.IsChecked == true;
|
_settings.RecordShowCursor = ShowCursorBox.IsChecked == true;
|
||||||
@@ -456,6 +478,7 @@ public partial class SettingsWindow : Window
|
|||||||
_settings.ToolbarPosition = defaults.ToolbarPosition;
|
_settings.ToolbarPosition = defaults.ToolbarPosition;
|
||||||
_settings.ToolbarLocked = defaults.ToolbarLocked;
|
_settings.ToolbarLocked = defaults.ToolbarLocked;
|
||||||
_settings.ToolbarOrder = defaults.ToolbarOrder;
|
_settings.ToolbarOrder = defaults.ToolbarOrder;
|
||||||
|
_settings.SplitToolbarOrder = defaults.SplitToolbarOrder;
|
||||||
_settings.ToolbarHidden = defaults.ToolbarHidden;
|
_settings.ToolbarHidden = defaults.ToolbarHidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,6 +523,11 @@ public partial class SettingsWindow : Window
|
|||||||
|
|
||||||
private void CheckUpdate_Click(object sender, RoutedEventArgs e) => _ = CheckForUpdatesAsync();
|
private void CheckUpdate_Click(object sender, RoutedEventArgs e) => _ = CheckForUpdatesAsync();
|
||||||
|
|
||||||
|
private void OnUpdateAvailabilityChanged(object? sender, EventArgs e) => ShowUpdateBadge();
|
||||||
|
|
||||||
|
private void ShowUpdateBadge() =>
|
||||||
|
UpdateTabBadge.Visibility = App.Instance.Updates.IsUpdateAvailable ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
private void ReleasePage_Click(object sender, RoutedEventArgs e) => OpenUrl(UpdateChecker.ReleasesPage);
|
private void ReleasePage_Click(object sender, RoutedEventArgs e) => OpenUrl(UpdateChecker.ReleasesPage);
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task CheckForUpdatesAsync()
|
private async System.Threading.Tasks.Task CheckForUpdatesAsync()
|
||||||
@@ -543,6 +571,7 @@ public partial class SettingsWindow : Window
|
|||||||
{
|
{
|
||||||
Version current = UpdateChecker.CurrentVersion;
|
Version current = UpdateChecker.CurrentVersion;
|
||||||
List<ReleaseInfo> newer = releases.Where(r => r.Version > current).ToList();
|
List<ReleaseInfo> newer = releases.Where(r => r.Version > current).ToList();
|
||||||
|
App.Instance.Updates.Report(newer.FirstOrDefault());
|
||||||
if (newer.Count == 0)
|
if (newer.Count == 0)
|
||||||
{
|
{
|
||||||
UpdateStatus.Text = releases.Count == 0
|
UpdateStatus.Text = releases.Count == 0
|
||||||
@@ -553,6 +582,14 @@ public partial class SettingsWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (AppIdentity.IsTest)
|
||||||
|
{
|
||||||
|
UpdateStatus.Text = $"Versie {newer[0].Version} is beschikbaar. Deze testversie werkt zichzelf niet bij; " +
|
||||||
|
"de installer zou de gewone kniip overschrijven.";
|
||||||
|
ShowChangelog(newer, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_update = newer.FirstOrDefault(r => r.InstallerUrl != null);
|
_update = newer.FirstOrDefault(r => r.InstallerUrl != null);
|
||||||
if (_update != null)
|
if (_update != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#0067C0" />
|
||||||
|
<SolidColorBrush x:Key="AccentHoverBrush" Color="#1975C5" />
|
||||||
|
<SolidColorBrush x:Key="AccentPressedBrush" Color="#3183CA" />
|
||||||
|
<SolidColorBrush x:Key="AccentTextBrush" Color="#4CC2FF" />
|
||||||
|
<SolidColorBrush x:Key="SelectedBrush" Color="#334CC2FF" />
|
||||||
|
<SolidColorBrush x:Key="WindowBrush" Color="#202020" />
|
||||||
|
<SolidColorBrush x:Key="ToolbarBrush" Color="#2B2B2B" />
|
||||||
|
<SolidColorBrush x:Key="UnlockedToolbarBrush" Color="#1F3347" />
|
||||||
|
<SolidColorBrush x:Key="CardBrush" Color="#2B2B2B" />
|
||||||
|
<SolidColorBrush x:Key="CardBorderBrush" Color="#3A3A3A" />
|
||||||
|
<SolidColorBrush x:Key="ToastBrush" Color="#2B2B2B" />
|
||||||
|
<SolidColorBrush x:Key="ToastBorderBrush" Color="#454545" />
|
||||||
|
<SolidColorBrush x:Key="HoverBrush" Color="#14FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="PressedBrush" Color="#1FFFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="CheckedBrush" Color="#3A3A3A" />
|
||||||
|
<SolidColorBrush x:Key="DividerBrush" Color="#3A3A3A" />
|
||||||
|
<SolidColorBrush x:Key="InkBrush" Color="#F2F2F2" />
|
||||||
|
<SolidColorBrush x:Key="SubtleTextBrush" Color="#A8A8A8" />
|
||||||
|
<SolidColorBrush x:Key="ControlBrush" Color="#2D2D2D" />
|
||||||
|
<SolidColorBrush x:Key="ControlHoverBrush" Color="#353535" />
|
||||||
|
<SolidColorBrush x:Key="ControlPressedBrush" Color="#3D3D3D" />
|
||||||
|
<SolidColorBrush x:Key="ControlBorderBrush" Color="#3F3F3F" />
|
||||||
|
<SolidColorBrush x:Key="ControlStrongBorderBrush" Color="#4A4A4A" />
|
||||||
|
<SolidColorBrush x:Key="CheckBoxBorderBrush" Color="#9A9A9A" />
|
||||||
|
<SolidColorBrush x:Key="ProminentBrush" Color="#383838" />
|
||||||
|
<SolidColorBrush x:Key="ProminentHoverBrush" Color="#424242" />
|
||||||
|
<SolidColorBrush x:Key="ProminentPressedBrush" Color="#303030" />
|
||||||
|
<SolidColorBrush x:Key="CanvasBrush" Color="#1C1C1C" />
|
||||||
|
<SolidColorBrush x:Key="CanvasBorderBrush" Color="#3A3A3A" />
|
||||||
|
<SolidColorBrush x:Key="TrackBrush" Color="#3A3A3A" />
|
||||||
|
<SolidColorBrush x:Key="CodeBrush" Color="#333333" />
|
||||||
|
<SolidColorBrush x:Key="QuoteBorderBrush" Color="#505050" />
|
||||||
|
|
||||||
|
<!-- Standaardbesturingselementen van WPF lezen deze systeemkleuren -->
|
||||||
|
<SolidColorBrush x:Key="{x:Static SystemColors.ControlTextBrushKey}" Color="#F2F2F2" />
|
||||||
|
<SolidColorBrush x:Key="{x:Static SystemColors.WindowTextBrushKey}" Color="#F2F2F2" />
|
||||||
|
<SolidColorBrush x:Key="{x:Static SystemColors.WindowBrushKey}" Color="#2D2D2D" />
|
||||||
|
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="#202020" />
|
||||||
|
<SolidColorBrush x:Key="{x:Static SystemColors.GrayTextBrushKey}" Color="#8A8A8A" />
|
||||||
|
<SolidColorBrush x:Key="{x:Static SystemColors.HotTrackBrushKey}" Color="#4CC2FF" />
|
||||||
|
|
||||||
|
<Style TargetType="ProgressBar">
|
||||||
|
<Setter Property="Background" Value="#3A3A3A" />
|
||||||
|
<Setter Property="BorderBrush" Value="#3A3A3A" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ScrollPageButton" TargetType="RepeatButton">
|
||||||
|
<Setter Property="Focusable" Value="False" />
|
||||||
|
<Setter Property="IsTabStop" Value="False" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RepeatButton">
|
||||||
|
<Border Background="Transparent" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ScrollThumb" TargetType="Thumb">
|
||||||
|
<Setter Property="Focusable" Value="False" />
|
||||||
|
<Setter Property="IsTabStop" Value="False" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Thumb">
|
||||||
|
<Border x:Name="Bd" Background="#5C5C5C" CornerRadius="3" Margin="3" />
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#7A7A7A" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsDragging" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#8E8E8E" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ScrollBar">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="Width" Value="12" />
|
||||||
|
<Setter Property="MinWidth" Value="12" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Grid Background="{TemplateBinding Background}">
|
||||||
|
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||||
|
<Track.DecreaseRepeatButton>
|
||||||
|
<RepeatButton Style="{StaticResource ScrollPageButton}" Command="ScrollBar.PageUpCommand" />
|
||||||
|
</Track.DecreaseRepeatButton>
|
||||||
|
<Track.IncreaseRepeatButton>
|
||||||
|
<RepeatButton Style="{StaticResource ScrollPageButton}" Command="ScrollBar.PageDownCommand" />
|
||||||
|
</Track.IncreaseRepeatButton>
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb Style="{StaticResource ScrollThumb}" />
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="Orientation" Value="Horizontal">
|
||||||
|
<Setter Property="Width" Value="Auto" />
|
||||||
|
<Setter Property="MinWidth" Value="0" />
|
||||||
|
<Setter Property="Height" Value="12" />
|
||||||
|
<Setter Property="MinHeight" Value="12" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Grid Background="{TemplateBinding Background}">
|
||||||
|
<Track x:Name="PART_Track">
|
||||||
|
<Track.DecreaseRepeatButton>
|
||||||
|
<RepeatButton Style="{StaticResource ScrollPageButton}" Command="ScrollBar.PageLeftCommand" />
|
||||||
|
</Track.DecreaseRepeatButton>
|
||||||
|
<Track.IncreaseRepeatButton>
|
||||||
|
<RepeatButton Style="{StaticResource ScrollPageButton}" Command="ScrollBar.PageRightCommand" />
|
||||||
|
</Track.IncreaseRepeatButton>
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb Style="{StaticResource ScrollThumb}" />
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="GridViewColumnHeader">
|
||||||
|
<Setter Property="Foreground" Value="#F2F2F2" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="GridViewColumnHeader">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="Bd" Background="#2B2B2B" BorderBrush="#3A3A3A" BorderThickness="0,0,1,1" Padding="6,4">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="Center" RecognizesAccessKey="True" />
|
||||||
|
</Border>
|
||||||
|
<Thumb x:Name="PART_HeaderGripper" HorizontalAlignment="Right" Width="8" Margin="0,0,-4,0" Cursor="SizeWE">
|
||||||
|
<Thumb.Template>
|
||||||
|
<ControlTemplate TargetType="Thumb">
|
||||||
|
<Border Background="Transparent" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Thumb.Template>
|
||||||
|
</Thumb>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#333333" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Role" Value="Padding">
|
||||||
|
<Setter TargetName="PART_HeaderGripper" Property="Visibility" Value="Collapsed" />
|
||||||
|
<Setter TargetName="Bd" Property="BorderThickness" Value="0,0,0,1" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#0067C0" />
|
||||||
|
<SolidColorBrush x:Key="AccentHoverBrush" Color="#1975C5" />
|
||||||
|
<SolidColorBrush x:Key="AccentPressedBrush" Color="#3183CA" />
|
||||||
|
<SolidColorBrush x:Key="AccentTextBrush" Color="#0067C0" />
|
||||||
|
<SolidColorBrush x:Key="SelectedBrush" Color="#1A0067C0" />
|
||||||
|
<SolidColorBrush x:Key="WindowBrush" Color="#F3F3F3" />
|
||||||
|
<SolidColorBrush x:Key="ToolbarBrush" Color="#F9F9F9" />
|
||||||
|
<SolidColorBrush x:Key="UnlockedToolbarBrush" Color="#EAF2FB" />
|
||||||
|
<SolidColorBrush x:Key="CardBrush" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="CardBorderBrush" Color="#E5E5E5" />
|
||||||
|
<SolidColorBrush x:Key="ToastBrush" Color="#FCFCFC" />
|
||||||
|
<SolidColorBrush x:Key="ToastBorderBrush" Color="#DADADA" />
|
||||||
|
<SolidColorBrush x:Key="HoverBrush" Color="#0F000000" />
|
||||||
|
<SolidColorBrush x:Key="PressedBrush" Color="#18000000" />
|
||||||
|
<SolidColorBrush x:Key="CheckedBrush" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="DividerBrush" Color="#E5E5E5" />
|
||||||
|
<SolidColorBrush x:Key="InkBrush" Color="#1A1A1A" />
|
||||||
|
<SolidColorBrush x:Key="SubtleTextBrush" Color="#5C5C5C" />
|
||||||
|
<SolidColorBrush x:Key="ControlBrush" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="ControlHoverBrush" Color="#F5F5F5" />
|
||||||
|
<SolidColorBrush x:Key="ControlPressedBrush" Color="#EBEBEB" />
|
||||||
|
<SolidColorBrush x:Key="ControlBorderBrush" Color="#E0E0E0" />
|
||||||
|
<SolidColorBrush x:Key="ControlStrongBorderBrush" Color="#D6D6D6" />
|
||||||
|
<SolidColorBrush x:Key="CheckBoxBorderBrush" Color="#8A8A8A" />
|
||||||
|
<SolidColorBrush x:Key="ProminentBrush" Color="#ECECEC" />
|
||||||
|
<SolidColorBrush x:Key="ProminentHoverBrush" Color="#E0E0E0" />
|
||||||
|
<SolidColorBrush x:Key="ProminentPressedBrush" Color="#D4D4D4" />
|
||||||
|
<SolidColorBrush x:Key="CanvasBrush" Color="#E9E9E9" />
|
||||||
|
<SolidColorBrush x:Key="CanvasBorderBrush" Color="#C8C8C8" />
|
||||||
|
<SolidColorBrush x:Key="TrackBrush" Color="#DADADA" />
|
||||||
|
<SolidColorBrush x:Key="CodeBrush" Color="#F3F3F3" />
|
||||||
|
<SolidColorBrush x:Key="QuoteBorderBrush" Color="#D0D0D0" />
|
||||||
|
</ResourceDictionary>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
@@ -25,8 +25,8 @@ public static class MarkdownRenderer
|
|||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
private static readonly FontFamily CodeFont = new("Cascadia Mono, Consolas");
|
private static readonly FontFamily CodeFont = new("Cascadia Mono, Consolas");
|
||||||
private static readonly Brush CodeBackground = Freeze(new SolidColorBrush(Color.FromRgb(0xF3, 0xF3, 0xF3)));
|
private static Brush CodeBackground => Resource("CodeBrush", Brushes.WhiteSmoke);
|
||||||
private static readonly Brush QuoteBorder = Freeze(new SolidColorBrush(Color.FromRgb(0xD0, 0xD0, 0xD0)));
|
private static Brush QuoteBorder => Resource("QuoteBorderBrush", Brushes.LightGray);
|
||||||
|
|
||||||
private static Brush Accent => Resource("AccentBrush", Brushes.SteelBlue);
|
private static Brush Accent => Resource("AccentBrush", Brushes.SteelBlue);
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ public static class MarkdownRenderer
|
|||||||
FontSize = 13,
|
FontSize = 13,
|
||||||
PagePadding = new Thickness(14, 10, 14, 10),
|
PagePadding = new Thickness(14, 10, 14, 10),
|
||||||
TextAlignment = TextAlignment.Left,
|
TextAlignment = TextAlignment.Left,
|
||||||
Background = Brushes.White,
|
Background = Resource("CardBrush", Brushes.White),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>Voegt de blokken van <paramref name="markdown"/> toe; relatieve links wijzen naar <paramref name="baseUri"/>.</summary>
|
/// <summary>Voegt de blokken van <paramref name="markdown"/> toe; relatieve links wijzen naar <paramref name="baseUri"/>.</summary>
|
||||||
@@ -342,10 +342,4 @@ public static class MarkdownRenderer
|
|||||||
|
|
||||||
private static Brush Resource(string key, Brush fallback) =>
|
private static Brush Resource(string key, Brush fallback) =>
|
||||||
Application.Current?.TryFindResource(key) as Brush ?? fallback;
|
Application.Current?.TryFindResource(key) as Brush ?? fallback;
|
||||||
|
|
||||||
private static Brush Freeze(SolidColorBrush brush)
|
|
||||||
{
|
|
||||||
brush.Freeze();
|
|
||||||
return brush;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace Kniip.Update;
|
||||||
|
|
||||||
|
/// <summary>Kijkt op de achtergrond of er een nieuwere versie is en meldt dat aan de vensters.</summary>
|
||||||
|
public sealed class UpdateNotifier
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan FirstCheckDelay = TimeSpan.FromSeconds(10);
|
||||||
|
private static readonly TimeSpan CheckInterval = TimeSpan.FromHours(24);
|
||||||
|
|
||||||
|
private readonly DispatcherTimer _timer = new();
|
||||||
|
private bool _enabled;
|
||||||
|
private bool _checking;
|
||||||
|
|
||||||
|
public UpdateNotifier()
|
||||||
|
{
|
||||||
|
_timer.Tick += async (_, _) =>
|
||||||
|
{
|
||||||
|
_timer.Interval = CheckInterval;
|
||||||
|
await CheckAsync();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nieuwste versie die nieuwer is dan deze exe; null als er niets nieuws is.</summary>
|
||||||
|
public ReleaseInfo? Available { get; private set; }
|
||||||
|
|
||||||
|
public bool IsUpdateAvailable => Available != null;
|
||||||
|
|
||||||
|
/// <summary>Wordt op de UI-thread afgevuurd als <see cref="Available"/> verandert.</summary>
|
||||||
|
public event EventHandler? AvailabilityChanged;
|
||||||
|
|
||||||
|
public void Apply(bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled == _enabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_enabled = enabled;
|
||||||
|
_timer.Stop();
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
_timer.Interval = FirstCheckDelay;
|
||||||
|
_timer.Start();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Report(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Resultaat van een controle elders (de tab Update), zodat de melding gelijkloopt.</summary>
|
||||||
|
public void Report(ReleaseInfo? newest)
|
||||||
|
{
|
||||||
|
if (newest != null && newest.Version <= UpdateChecker.CurrentVersion)
|
||||||
|
{
|
||||||
|
newest = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = Available?.Version != newest?.Version;
|
||||||
|
Available = newest;
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
AvailabilityChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckAsync()
|
||||||
|
{
|
||||||
|
if (_checking)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_checking = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IReadOnlyList<ReleaseInfo> releases = await UpdateChecker.GetReleasesAsync();
|
||||||
|
if (_enabled)
|
||||||
|
{
|
||||||
|
Report(releases.FirstOrDefault(r => r.Version > UpdateChecker.CurrentVersion));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (UpdateException)
|
||||||
|
{
|
||||||
|
// Een mislukte achtergrondcontrole is geen melding waard; de volgende poging komt vanzelf.
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_checking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Maakt src/Kniip/TrayIcon.ico uit de 256px-afbeelding in AppIcon.ico. Het systeemvak tekent op 16px;
|
||||||
|
# daar zijn de snijlijnen en de zwarte rand op een donkere taakbalk onzichtbaar en lijkt het icoon klein.
|
||||||
|
# Dit snijdt alleen het fotokader uit, zodat dat het hele vak vult. Opnieuw draaien als AppIcon.ico wijzigt.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$root = Split-Path -Parent $PSScriptRoot
|
||||||
|
$source = Join-Path $root 'src\Kniip\AppIcon.ico'
|
||||||
|
$target = Join-Path $root 'src\Kniip\TrayIcon.ico'
|
||||||
|
|
||||||
|
# Kader in de 256px-afbeelding, inclusief de zwarte rand.
|
||||||
|
$crop = New-Object System.Drawing.Rectangle 42, 42, 172, 172
|
||||||
|
$sizes = 16, 20, 24, 32, 40, 48, 64
|
||||||
|
|
||||||
|
function Get-IcoPng([byte[]]$ico, [int]$size) {
|
||||||
|
$count = [BitConverter]::ToUInt16($ico, 4)
|
||||||
|
for ($i = 0; $i -lt $count; $i++) {
|
||||||
|
$entry = 6 + $i * 16
|
||||||
|
$width = if ($ico[$entry] -eq 0) { 256 } else { $ico[$entry] }
|
||||||
|
if ($width -eq $size) {
|
||||||
|
$length = [BitConverter]::ToUInt32($ico, $entry + 8)
|
||||||
|
$offset = [BitConverter]::ToUInt32($ico, $entry + 12)
|
||||||
|
$data = New-Object byte[] $length
|
||||||
|
[Array]::Copy($ico, $offset, $data, 0, $length)
|
||||||
|
return $data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw "Geen ${size}px-afbeelding in $source"
|
||||||
|
}
|
||||||
|
|
||||||
|
$png = Get-IcoPng ([IO.File]::ReadAllBytes($source)) 256
|
||||||
|
$master = [System.Drawing.Bitmap]::FromStream((New-Object IO.MemoryStream (, $png)))
|
||||||
|
|
||||||
|
$images = foreach ($size in $sizes) {
|
||||||
|
$bitmap = New-Object System.Drawing.Bitmap $size, $size, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
$g = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||||
|
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||||
|
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||||
|
$g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
|
||||||
|
$g.DrawImage($master, (New-Object System.Drawing.Rectangle 0, 0, $size, $size), $crop, [System.Drawing.GraphicsUnit]::Pixel)
|
||||||
|
$g.Dispose()
|
||||||
|
$stream = New-Object IO.MemoryStream
|
||||||
|
$bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$bitmap.Dispose()
|
||||||
|
, $stream.ToArray()
|
||||||
|
}
|
||||||
|
$master.Dispose()
|
||||||
|
|
||||||
|
$out = New-Object IO.MemoryStream
|
||||||
|
$writer = New-Object IO.BinaryWriter $out
|
||||||
|
$writer.Write([UInt16]0)
|
||||||
|
$writer.Write([UInt16]1)
|
||||||
|
$writer.Write([UInt16]$sizes.Count)
|
||||||
|
$offset = 6 + 16 * $sizes.Count
|
||||||
|
for ($i = 0; $i -lt $sizes.Count; $i++) {
|
||||||
|
$writer.Write([byte]$sizes[$i])
|
||||||
|
$writer.Write([byte]$sizes[$i])
|
||||||
|
$writer.Write([byte]0)
|
||||||
|
$writer.Write([byte]0)
|
||||||
|
$writer.Write([UInt16]1)
|
||||||
|
$writer.Write([UInt16]32)
|
||||||
|
$writer.Write([UInt32]$images[$i].Length)
|
||||||
|
$writer.Write([UInt32]$offset)
|
||||||
|
$offset += $images[$i].Length
|
||||||
|
}
|
||||||
|
foreach ($image in $images) {
|
||||||
|
$writer.Write($image)
|
||||||
|
}
|
||||||
|
$writer.Flush()
|
||||||
|
[IO.File]::WriteAllBytes($target, $out.ToArray())
|
||||||
|
Write-Host "Geschreven: $target"
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$knownNames = @("Kniip", "KnipPlus")
|
$knownNames = @("Kniip", "KniipTest", "KnipPlus")
|
||||||
$exePattern = '(?i)(^|[\\/"])(Kniip|KnipPlus)\.exe(\b|")'
|
$exePattern = '(?i)(^|[\\/"])(Kniip(Test)?(-[\d.]+)?|KnipPlus)\.exe(\b|")'
|
||||||
|
|
||||||
function Test-KniipCommand([string]$text) {
|
function Test-KniipCommand([string]$text) {
|
||||||
return [bool]($text -match $exePattern)
|
return [bool]($text -match $exePattern)
|
||||||
|
|||||||
Reference in new issue
Block a user